Les processeurs vus auparavant ne peuvent émettre au maximum qu'une instruction par cycle d'horloge : ce sont des processeurs à émission unique. Et quand on court après la performance, on en veut toujours plus : un IPC de 1, c'est pas assez ! Pour cela, les concepteurs de processeurs ont inventés des processeurs qui émettent plusieurs instructions par cycle : les processeurs à émissions multiples.
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 que ces derniers ont un jeu d'instruction spécialisé qui expose le parallélisme d'instruction directement au niveau des instructions, là où les processeurs superscalaires restent des processeurs au jeu d'instruction normal. La différence principale entre les deux 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. Certains processeurs superscalaires n'utilisent pas l'exécution dans le désordre, tandis que d'autres le font.
Pour qu'un processeur superscalaire répartisse ses instructions sur plusieurs unités de calcul, il faut modifier toutes les étapes entre le chargement et les unités de calcul. Un processeur superscalaire charge plusieurs instructions en même temps, les décode en parallèle, puis les exécute sur des unités de calculs séparées. Du moins, c'est le cas si tout se passe bien. Dans la réalité, il faut gérer les dépendances entre instructions. Sans compter que l'émission multiple demande de répartir les instructions sur différentes unités de calcul, et cela n'est pas une mince affaire.
L'étape de chargement superscalaire.
Sur les processeurs superscalaires, l'unité de chargement charge un bloc de mémoire de taille fixe, qui contient plusieurs instructions (le "program counter" est modifié en conséquence). Ceci dit, il faut convenablement gérer le cas où un branchement pris se retrouve en plein milieu d'un bloc. Dans ce qui va suivre, un morceau de code sans branchement est appelé un bloc de base ("basic block").
Certains processeurs superscalaires exécutent toutes les instructions d'un bloc, sauf celles qui suivent un 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. Le processeur détermine quels branchements sont pris ou non avec la prédiction de branchements.
D'autres chargent 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 (exécutées). Le principe peut se généraliser avec un nombre de blocs supérieur à deux.
Ces processeurs utilisent des unités de prédiction de branchement capables de prédire plusieurs branchements par cycle, au minimum l'adresse du bloc à charger et la ou les adresses de destination des branchements dans le bloc. De plus, on doit charger deux blocs de mémoire en une fois, via des caches d'instruction multiports. Il faut aussi ajouter un circuit pour assembler plusieurs morceaux de blocs en un seul : le fusionneur ("merger"). Le résultat en sortie du fusionneur est ce qu'on appelle une trace.
Le 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. Mais il reste à déterminer si une trace peut être réutilisée. Une trace est réutilisable quand le premier bloc de base est identique et que les prédictions de branchement restent identiques. Dans ces conditions, le tag du cache de traces doit contenir l'adresse de départ 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 utilisées pour construire 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 l'adresse de chargement au cache de traces, le reste des informations étant fournie par l'unité de prédiction de branchement. Si le tag est identique, alors 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.
Certains caches de traces peuvent stocker plusieurs traces différentes pour une même adresse de départ, avec une trace par ensemble de prédiction. Mais d'autres caches de traces n'autorisent qu'une seule trace par adresse de départ, ce qui est sous-optimal. Mais on peut limiter la casse on utilisant des correspondances partielles. Si jamais les prédictions de branchement et la position des branchements n'est pas strictement identique, il arrive quand même que les premières prédictions et les premiers branchements soient les mêmes. Dans ce cas, on peut alors réutiliser les blocs de base concernés et le processeur charge les portions de la trace qui sont valides depuis le cache de traces. Une autre solution consiste à reconstituer les traces à la volée. Il suffit de mémoriser les blocs de base dans des caches dédiés et les assembler par un fusionneur. Par exemple, au lieu d'utiliser un cache de traces dont chaque ligne peut contenir quatre blocs de base, on va utiliser quatre caches de blocs de base. Cela permet de supprimer la redondance que l'on trouve dans les traces, quand elles se partagent des blocs de base identiques, ce qui est avantageux à mémoire égale.
La présence d'un cache de traces se marie très bien avec une unité de prédiction de branchement capable de prédire un grand nombre de branchements par cycle. Malheureusement, ces unités de prédiction de branchement sont très complexes et gourmandes en circuits. Les concepteurs de processeurs préfèrent utiliser une unité de prédiction de branchement normale, qui ne peut prédire l'adresse que d'un seul bloc de base. Pour pouvoir utiliser un cache de traces avec une unité de prédiction aussi simple, les concepteurs de processeurs vont ajouter une seconde unité de prédiction, spécialisée dans le cache de traces.
Les décodeurs d'instructions superscalaires.
Le séquenceur d'un processeur superscalaire est lui aussi modifié, afin de pourvoir décoder plusieurs instructions à la fois. Typiquement, il contient 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. ce n'est cependant pas le cas général, mais la présence de plusieurs décodeurs est très fréquent sur les processeur superscalaires.
Pour donner un exemple assez particulier, prenons le cas des processeurs RISC-V. Sur ce jeu d'instruction, il existe des instructions longues de 32 bits et des instructions courtes de 16 bits. Le chargement des instructions se fait par blocs de 32 bits, ce qui permet de charger une instruction de 32 bits, ou deux instructions de 16 bits. Pour simplifier, supposons que les instructions sont alignées sur des blocs de 32 bits, ce qui signifie qu'un bloc chargé contient soit une instruction de 32 bits, soit deux instructions de 16 bits. On ne tient pas compte des cas où une instruction de 16 bit est immédiatement suivie par une instruction de 32 bits à cheval sur deux blocs. Il est possible de créer un processeur RISC-V partiellement superscalaire en utilisant deux voies : une avec un décodeur pour les instructions 32 bits et une autre voie contenant deux décodeurs pour les instructions de 16 bits. Ainsi, lors du chargement d'un bloc de deux instructions courtes, les deux instructions sont chacune décodées par un décodeur séparé.
Il est même possible de décoder les instructions dans les deux voies en parallèle, avant de choisir quel est celui qui a raison. Sans cette méthode, on doit identifier si le bloc contient une instruction longue ou deux instructions courtes, avant d'envoyer les instructions au décodeur adéquat. Mais avec cette méthode, l'identification du nombre d'instruction se fait en parallèle du décodage proprement dit. Évidemment, une des deux voies donnera des résultats aberrants et totalement faux, mais l'autre donnera le bon résultat. Il suffit de choisir le bon résultat en sortie avec un multiplexeur.
Notons que certaines techniques permettent au décodeur de fusionner des instructions en une seule micro-opération. Par exemple, il est possible de fusionner une multiplication suivie d'une addition en une seule instruction MAD ("multiply and add"), si les conditions adéquates sont réunies pour les opérandes. Comme autre exemple, il est possible de fusionner un calcul d'adresse suivi d'une lecture à l'adresse calculée en une seule micro-opération d'accès mémoire. Cette technique n'est pas exclusive aux processeur superscalaires et fonctionne si tout processeur avec un pipeline, mais elle fonctionne au mieux sur ces processeurs, du fait de leur capacité à charger plusieurs instructions à la fois. Cette fusion est faite avant le décodage, quand les instructions sont encore dans l"'instruction queue", la "prefetch input queue", la mémoire tampon située entre l'étage de chargement et l'étage de décodage.
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.
Seules les dépendances lecture-après-écriture doivent être détectées, les autres étant supprimées par le renommage de registres. Repérer ce genre de dépendances se fait assez simplement : il suffit de regarder si un registre de destination d'une instruction est un opérande d'une instruction suivante.
Ensuite, il faut corriger le résultat du renommage en fonction des dépendances. Si une instruction n'a pas de dépendance avec une autre, on la laisse telle quelle. Dans le cas contraire, un registre opérande sera identique avec le registre de destination d'une instruction précédente. Dans ce cas, le registre opérande n'est pas le bon après renommage : on doit le remplacer par le registre de destination de l'instruction avec laquelle il y a dépendance. Cela se fait simplement en utilisant un multiplexeur dont les entrées sont reliées à l'unité de détection des dépendances. On doit faire ce replacement pour chaque registre opérande.
L'unité d'émission superscalaire.
Sur un processeur à émission multiple, l'unité d'émission doit, en plus de ses fonctions habituelles, détecter les dépendances entre instructions à émettre simultanément. L'unité d'émission d'un processeur superscalaire se voit donc ajouter un nouvel étage pour les dépendances entre instructions à émettre. 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.
L'accès au banc de registres.
É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é. Mais ces ports ajoutés sont souvent sous-utilisés en situation réelle. On peut en profiter pour ne pas utiliser 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é, l’arbitre du banc de registres ("register file arbiter").