Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC

Un livre de Wikilivres.

Dans les chapitres précédents, nous avons parlé des processeurs qui sont capables d’exécuter des instructions dans le désordre, ou plusieurs instructions en parallèle, en même temps. Mais nous nous sommes concentrés sur les processeurs avec un jeu d'instruction normal, où l’exécution des instructions en parallèle est invisible pour le programmeur ou le compilateur. De tels processeurs sont en apparence des processeurs séquentiels normaux, mais avec un parallélisme d'instruction (c’est-à-dire le fait de pouvoir exécuter plusieurs instructions en même temps). Les processeurs superscalaires sont de ce type, mais ils permettent non seulement d’exécuter plusieurs instructions en même temps, mais aussi d'émettre plusieurs instructions en même temps sur des unités de calcul différentes. Le parallélisme d'instruction maximal avec ce genre d’architectures est donc un processeur pipeliné, superscalaire, à exécution dans le désordre avec renommage de registre.

Mai ces architectures ont un défaut : elles doivent charger une séquence d’instructions, détecter les dépendances, et déterminer quelles sont les instructions exécutables en parallèle. Mais il se trouve que ces informations sur les dépendances d'instructions sont connues des compilateurs modernes. Lors de la compilation, le code source est traduit en une représentation intermédiaire où les dépendances de type WAR, WAW et RAR n'existent tout simplement pas. Il faut dire que la représentation intermédiaire a souvent une infinité de registres, voire fonctionne sans registres et avec une infinité de places mémoires. De plus, les dépendances RAW sont identifiées par le compilateur, qui doit savoir quel opérande est utilisée par quelle instruction, etc. De fait, les processeurs à exécution dans le désordre doivent retrouver des informations qui étaient connues du compilateur mais ont disparu du code source. De cette observation est venue l'idée de modifier le jeu d’instruction pour que ces informations sur les dépendances soient disponibles directement dans le code machine lui-même. Ainsi, le compilateur fait tout le travail de réorganisation des instructions et d’exécution en parallèle, à la place du processeur. De telles architectures s'appellent des architectures à parallélisme d'instruction explicite.

Il existe plusieurs types d'architectures de ce genre, les deux principales étant les architectures VLIW et dataflow. Les architectures dataflow seront vues dans le chapitre suivant. L'idée derrière ces architectures est d'expliciter les dépendances entre instruction et de les encoder directement dans le code machine. Le processeur peut ainsi déterminer quelle instruction est prête à s’exécuter en fonction de la disponibilité des opérandes. En clair, l’exécution dans le désordre est réalisée par le compilateur, au niveau du jeu d'instruction. Les processeurs VLIW et leurs dérivés sont moins ambitieux. Leur but est d'effectuer de l'émission multiple, à savoir exécuter plusieurs instructions indépendantes en parallèle, mais sans forcément avoir de l’exécution dans le désordre. Ils chargent "plusieurs instructions à la fois" et les exécutent sur des unités de calcul séparées (les guillemets sont là pour vous faire comprendre que c'est en réalité plus compliqué). Le compilateur garantit que les instructions chargées en même temps sont indépendantes et peuvent s’exécuter en parallèle. Dans ce chapitre, nous allons nous concentrer sur les processeurs VLIW.

Les processeurs VLIW[modifier | modifier le wikicode]

Les processeurs VLIW, ou very long instruction word exécutent des faisceaux d’instructions (aussi appelés bundle), des regroupements de plusieurs instructions qui s'exécutent en parallèle sur différentes unités de calcul. Le faisceau est chargé en une seule fois et est encodé comme une instruction unique. Les processeurs VLIW n'utilisent pas l'exécution dans le désordre, ce qui fait qu'ils ne peuvent rien contre les dépendances qui ne peuvent être supprimées qu'à l’exécution, comme celles liées aux accès mémoires.

Pipeline simplifié d'un processeur VLIW. On voit que le faisceau est chargé en un cycle d'horloge, mais que les instructions sont exécutées en même temps dans des unités de calcul séparées.

Chaque instruction d'un faisceau précise l'unité de calcul qui doit la prendre en charge. Et à ce petit jeu, il existe deux possibilités, respectivement nommées encodage par position et par nommage. Avec la première méthode, un faisceau est découpé en créneaux (slot), chacun étant attribué à une ALU. La position de l'instruction dans le faisceau détermine l'ALU à utiliser. Avec la seconde solution, chaque instruction contient un numéro qui indique l'unité de calcul à utiliser. Cette technique est déclinée en deux formes : soit on trouve un identifiant d'ALU par instruction, soit on utilise un identifiant pour tout le faisceau, qui permet à lui seul de déterminer l'unité associée à chaque instruction.

C'est le compilateur qui regroupe plusieurs instructions en un seul faisceau. Et il se peut qu'il ne puisse pas remplir tout le faisceau avec des instructions indépendantes. Sur les anciens processeurs VLIW, les instructions VLIW (les faisceaux) étaient de taille fixe, ce qui forçait le compilateur à remplir d'éventuels vides avec des NOP, diminuant la densité de code. La majorité des processeurs VLIW récents utilise des faisceaux de longueur variable, supprimant ces NOP.

Les dépendances intra-faisceaux[modifier | modifier le wikicode]

Des difficultés arrivent quand des instructions d'un faisceau manipulent le même registre. Les problèmes apparaissent avec des écritures, notamment quand une instruction lit un registre écrit par une autre instruction. Dans ce cas, le processeur peut gérer la situation de trois manières différentes :

  • la lecture du registre renvoie la valeur avant l'écriture ;
  • la lecture lit la valeur écrite par l'écriture ;
  • le processeur interdit aux instructions d'un même faisceau de lire et écrire le même registre.

Les exceptions[modifier | modifier le wikicode]

Lorsqu'une instruction lève une exception, un processeur VLIW peut gérer la situation de plusieurs manières différentes, qui dépendent du processeur. Soit :

  • on invalide toutes les instructions du faisceau ;
  • on invalide uniquement les instructions qui suivent l'exception dans l'ordre du programme (le compilateur doit ajouter quelques informations sur l'ordre des instructions dans le faisceau) ;
  • on invalide seulement les instructions qui ont une dépendance avec celle qui a levé l'exception ;
  • on exécute toutes les instructions du faisceau, sauf celle qui a levé l'exception.

Les défauts des architectures VLIW[modifier | modifier le wikicode]

Les architectures VLIW ont divers problèmes, comme une faible compatibilité, des performances limitées par les dépendances existantes à la compilation et une densité de code mauvaise.

Sur ces architectures, certaines dépendances entre instructions ne peuvent être supprimées, ce qu'un processeur à exécution dans le désordre peut faire. Par exemple, le fait que les accès à la mémoire aient des durées variables (suivant que la donnée soit dans le cache ou la RAM, par exemple) joue sur les différentes dépendances. Un compilateur ne peut pas savoir combien de temps va mettre un accès mémoire et il ne peut organiser les instructions d'un programme en conséquence. Par contre, un processeur le peu. Autre exemple : les dépendances d'instructions dues aux branchements, qui ont tendance à limiter fortement les possibilités d'optimisation du compilateur.

Autre défaut : les processeurs VLIW n'ont strictement aucune compatibilité, ou alors celle-ci est très limitée. En effet, le format des faisceaux VLIW est spécifique à un processeur. Celui-ci va dire : telle instruction va sur telle ALU, et pas ailleurs. Mais si on rajoute des unités de calcul dans une nouvelle version du processeur, il faudra recompiler notre programme pour que celui-ci puisse l'utiliser, voire simplement faire fonctionner notre programme. Dans des situations dans lesquelles on se moque de la compatibilité, cela ne pose aucun problème : par exemple, on utilise beaucoup les processeurs VLIW dans l'embarqué. Mais pour un ordinateur de bureau, c'est autre chose...

Les processeurs EPIC[modifier | modifier le wikicode]

En 1997, Intel et HP lancèrent un nouveau processeur, l'Itanium, dont l'architecture corrigeait les défauts des autres processeurs VLIW. Dans un but marketing évident, Intel et HP prétendirent que l'architecture de ce processeur n'était pas du VLIW amélioré, mais une nouvelle architecture appelée EPIC, pour Explicit Parallelism Instruction Computing. Il faut avouer que cette architecture avait de fortes différences avec le VLIW, mais aussi beaucoup de points communs. Bien évidemment, beaucoup ne furent pas dupes, et une gigantesque controverse vit le jour : est-ce que les architectures EPIC sont des VLIW ou non ? Sans prendre position sur le sujet, nous avons séparé les processeurs VLIW et EPIC, par souci didactique.

Les faisceaux des CPU EPIC[modifier | modifier le wikicode]

La première différence avec les VLIW tient dans les faisceaux. Ceux-ci deviennent des groupes d'instructions indépendantes, délimités par un petit groupe de bits d’arrêt (stop bits) qui indique la fin d'un faisceau.

Bits d’arrêt.

D'autres processeurs utilisent un bit de parallélisme (parallel bit), un bit placé à la fin d'une instruction qui dit si elle peut s'effectuer ou non en parallèle de la suivante.

Bit de parallélisme.

Avec l'usage d'un bit de stop ou de parallélisme, le processeur découpe le faisceau en instructions et les répartit sur les unités de calcul. L'avantage est que la compatibilité est nettement meilleure, de même que les performances. Peu importe le nombre d'unité de calcul, le processeur peut profiter de leur présence en répartissant les instructions convenablement. Enfin, on peut aussi citer les avantages en termes de densité de code : pas besoin d'insérer des NOPs dans des slots vides ou de fournir des identifiants d'ALUs, comme avec les processeurs VLIW.

Les exceptions différées[modifier | modifier le wikicode]

L'Itanium implémente ce qu'on appelle les exceptions différées. Avec cette technique, le compilateur crée deux versions d'un même code : une version optimisée qui suppose qu'aucune exception matérielle n'est levée, et une autre version qui prend en compte les exceptions. Le programme exécute d'abord la version optimisée de manière spéculative, mais annule son exécution et repasse sur la version non-optimisée en cas d'exception.

Pour vérifier l’occurrence d'une exception, chaque registre est associé à un bit « rien du tout » (not a thing), mis à 1 s'il contient une valeur invalide. Si une instruction lève une exception, elle écrira un résultat faux dans un registre et le bit « rien du tout » est mis à 1. Les autres instructions propageront ce bit « rien du tout » dans leurs résultats : si un opérande est « rien du tout », le résultat de l'instruction l'est aussi. Une fois le code fini, il suffit d'utiliser une instruction qui teste l'ensemble des bits « rien du tout » et agit en conséquence.

La spéculation sur les lectures[modifier | modifier le wikicode]

L'Itanium fournit une fonctionnalité similaire pour les lectures, où le code est compilé dans une version optimisée où les lectures sont déplacées sans tenir compte des dépendances, avec un code de secours non-optimisé. Encore une fois, le processeur vérifie si la spéculation s'est bien passée, avant de décider de passer sur le code de secours ou non. La vérification ne se fait pas de la même façon selon que la lecture ait été déplacée avant un branchement ou avant une autre écriture.

  • Si on passe une lecture avant un branchement, la lecture et la vérification sont effectuées par les instructions LD.S et CHK.S. Si une dépendance est violée, le processeur lève une exception différée : le bit « rien du tout » du registre contenant la donnée lue est alors mis à 1. CHK.S ne fait rien d'autre que vérifier ce bit.
  • Si on passe une lecture avant une écriture, la désambiguïsation de la mémoire est gérée par le compilateur. Tout se passe comme avec les branchements, à part que les instructions sont nommées LD.A et CHK.A.
Spéculation sur les lectures.

Pour détecter les violations de dépendance, le processeur maintient une liste des lectures spéculatives qui n'ont pas causé de dépendances mémoire, dans un cache : la table des adresses lues en avance (advanced load address table ou ALAT). Ce cache stocke l'adresse, la longueur de la donnée lue, le registre de destination, etc. Toute écriture vérifie si une lecture à la même adresse est présente dans l'ALAT : si c'est le cas, une dépendance a été violée, et la lecture est retirée de l'ALAT.

Les bancs de registres tournants[modifier | modifier le wikicode]

Les processeurs EPIC et VLIW utilisent une forme limitée de renommage de registres pour accélérer certaines boucles. Pour l’expliquer, prenons une boucle simple et intéressons-nous au corps de la boucle, à savoir la boucle sans les branchements et instructions de test qui servent à répéter les instructions. La boucle d'exemple se contente d'ajouter 5 à tous les éléments d'un tableau. L'adresse de l’élément du tableau est stockée dans le registre R2. Dans le code qui suivra, les crochets serviront à indiquer l'utilisation du mode d'adressage indirect. Sans optimisations, le corps de la boucle est le suivant :

loop :

   load R5 [R2] / add 4 R2 ;
   add 5 R5 ; 
   store [R2] R5 ;

Les différentes itérations de la boucle peuvent se calculer en parallèle, vu que les éléments du tableau sont manipulés indépendamment. Mais codée comme dessus, ce n'est pas possible car les trois instructions de la boucle utilisent le registre R5 et ont donc des dépendances. Le renommage de registres peut éliminer ces dépendances, mais il n'est pas disponible sur les processeurs VLIW et EPIC. À la place, les concepteurs de processeurs ont inventé les bancs de registres tournants (rotating register files). Avec cette méthode, la correspondance (nom de registre - registre physique) se décale d'un cran à chaque cycle d’horloge. Par exemple, le registre nommé R0 à un instant donné devient le registre R1 au cycle d'après, et idem pour tous les registres. Précisons que sur l'Itanium, cette technique est appliquée non pas à l'ensemble du banc de registre, mais est limitée à un banc de registres spécialisé dans l’exécution des boucles. Évidemment, le code source du programme doit être modifié pour en tenir compte. Ainsi, le code vu précédemment devient celui-ci.

loop :

   load RB5 [R2] / add 4 R2 ;
   add 5 RB6 ; 
   store [R2] RB7 ;

Ainsi, le LOAD d'une itération ne touchera pas le même registre que le LOAD de l'itération suivante, idem pour l'instruction de calcul et le STORE. Le nom de registre sera le même, mais le fait que les noms de registre se décalent à chaque cycle d'horloge fera que ces noms identiques correspondent à des registres différents. Les dépendances sont supprimées, et le pipeline est utilisé à pleine puissance.

Cette technique s'implémente avec un simple compteur, incrémenté à chaque cycle d'horloge, qui mémorise le décalage à appliquer aux noms de registre. À chaque utilisation d'un registre, le contenu de ce compteur est ajouté au nom de registre à accéder.