Le niveau microarchitecture montre comment un cœur moderne trouve seul du parallélisme : il regarde des dizaines d’instructions en avant, renomme les registres, lance ce qui est prêt, et retire tout dans l’ordre. Ce matériel est gros et gourmand en énergie. Depuis les années 1980, une idée revient régulièrement : faire ce travail une fois, dans le compilateur, plutôt que des milliards de fois par seconde dans le silicium. Une ISA construite sur cette idée doit dire plus que « exécute ces instructions dans l’ordre » : elle doit permettre au compilateur d’indiquer quelles instructions peuvent s’exécuter ensemble.
C’est le VLIW (very long instruction word, mot d’instruction très long), et sa version la plus ambitieuse, EPIC, a été le pari d’Intel et HP pour remplacer le x86. Le livre de Tanenbaum la présente longuement et avec enthousiasme. Elle mérite d’être étudiée pour ses idées, et pour ce qui s’est passé ensuite.
Le VLIW : le compilateur ordonnance
Une instruction VLIW est un paquet (bundle) de plusieurs opérations, une par unité fonctionnelle, toutes lancées au même cycle. Le compilateur garantit qu’elles sont indépendantes. Le matériel ne vérifie pas les dépendances, ne réordonne pas, ne renomme pas : il envoie simplement chaque case à son unité. Si le compilateur ne trouve rien d’utile pour une case, il la remplit d’une instruction vide.
Le bénéfice : un matériel simple et large. Les coûts : la taille du code (toutes ces instructions vides) et un couplage étroit entre le binaire et la puce : un programme ordonnancé pour une machine à deux multiplieurs et à chargements de 3 cycles est faux, ou au moins lent, sur une machine à trois multiplieurs et à chargements de 4 cycles. Les premières machines VLIW, comme celles de Multiflow dans les années 1980, imposaient de recompiler pour chaque nouveau modèle.
EPIC et l’IA-64
L’IA-64 d’Intel et HP, commercialisée d’abord sous le nom d’Itanium en 2001, appelait sa version EPIC : explicitly parallel instruction computing. Elle gardait l’idée du VLIW — le compilateur trouve le parallélisme — mais tentait d’en corriger les faiblesses :
- Des paquets de 128 bits contiennent trois instructions de 41 bits et un gabarit (template) de 5 bits. Le gabarit indique de quelles sortes d’unités les trois instructions ont besoin, et où se terminent les groupes d’instructions : des suites d’instructions, éventuellement sur plusieurs paquets, que le compilateur garantit indépendantes. Comme les groupes ne sont pas liés à la largeur de la machine, le même binaire tourne sur des implémentations plus larges ou plus étroites.
- Beaucoup de registres : 128 registres entiers et 128 registres flottants, pour que le compilateur n’en manque jamais et n’ait jamais besoin de renommage. 96 des registres entiers forment une pile de registres : chaque fonction alloue exactement les registres dont elle a besoin, et les appels décalent la fenêtre au lieu de sauvegarder les registres en mémoire.
- La prédication : 64 registres de prédicat d’un bit, et chaque instruction en désigne un. Une comparaison positionne un prédicat et son complément ; les instructions du « alors » et du « sinon » s’exécutent alors côte à côte, chacune gardée par son prédicat, et seul le côté vrai écrit son résultat. Le branchement disparaît, et avec lui le risque de le mal prédire.
- Les chargements spéculatifs : le compilateur peut remonter un chargement au-dessus du branchement qui le protège (
ld.s). Si le chargement devait provoquer une faute — par exemple parce que le pointeur est nul sur le chemin non pris —, il ne la provoque pas : il marque son registre de destination comme invalide (un bit « NaT », le bit de poison du chapitre sur l’exécution dans le désordre), et une instruction de vérification ultérieure (chk.s) ne lève la faute que si la valeur est réellement utilisée. Les chargements anticipés (ld.a) vont plus loin et remontent un chargement au-dessus d’un rangement qui pourrait écrire à la même adresse ; une table matérielle surveille les rangements, et une vérification recharge la valeur si l’un d’eux l’a touchée.
La section du livre sur l’IA-64 soutient que le x86 était arrivé au bout du chemin, et conclut que déplacer du travail de l’exécution vers la compilation est « toujours gagnant ». L’histoire a pris l’autre direction.
Là où l’ordonnancement à la compilation échoue
Les deux démos ci-dessous exécutent le même travail — trois chargements et les calculs qui en dépendent — sur le simulateur d’exécution dans le désordre, dont le réglage in order se comporte comme une machine ordonnancée statiquement : il ne lance jamais une instruction avant celles qui la précèdent.
Dans la première, les instructions sont dans l’ordre du source : chaque chargement est suivi de l’instruction qui l’utilise.
À essayer : Passez de l’exécution dans l’ordre au désordre, changez la largeur, la fenêtre, la latence des chargements (4 = L1, 12 = L2, 40 = L3) et le renommage ci-dessous, et comparez les cycles — ou appuyez sur Edit pour essayer votre propre code.
Loading emulator…
Dans l’ordre, chaque chargement attend derrière l’utilisation du précédent : 19 cycles quand les chargements trouvent leur donnée dans le cache L1 (latence de 4 cycles). Réglez maintenant la latence des chargements sur 40, un aller-retour jusqu’au cache L3 : 127 cycles dans l’ordre, car les trois chargements attendent l’un après l’autre. Dans le désordre, le matériel lance les trois chargements à la fois : 12 et 48 cycles.
Dans la seconde, le compilateur a fait d’avance le travail du cœur à exécution dans le désordre, en remontant les trois chargements indépendants en tête :
À essayer : Passez de l’exécution dans l’ordre au désordre, changez la largeur, la fenêtre, la latence des chargements (4 = L1, 12 = L2, 40 = L3) et le renommage ci-dessous, et comparez les cycles — ou appuyez sur Edit pour essayer votre propre code.
Loading emulator…
Dans l’ordre comme dans le désordre, le temps est désormais exactement le même à toutes les latences : 12, 20 et 48 cycles pour 4, 12 et 40. C’est le postulat d’EPIC, et ici il tient.
Il tient parce que ce code est facile : trois chargements à des adresses fixes, sans branchement ni pointeur. Les vrais programmes laissent rarement le compilateur faire cela :
- Les pointeurs peuvent se recouvrir (aliasing). Si un rangement par
pse trouve entre les chargements et que le compilateur ne peut pas prouver queppointe ailleurs, les chargements ne peuvent pas remonter au-dessus. Les chargements anticipés aident, au prix d’instructions de vérification et de code de reprise. - Les branchements bloquent les déplacements. Les chargements situés derrière un
ifne peuvent remonter que de façon spéculative, avec la même mécanique de vérification et de reprise. - La latence est inconnue. Qu’un chargement prenne 4 cycles ou 300 dépend du cache, qui dépend des données et de ce qui tourne à côté. Le compilateur doit choisir un seul ordonnancement pour tous les cas ; le matériel à exécution dans le désordre s’adapte à chaque exécution, à la latence réellement subie.
Le cœur à exécution dans le désordre fait aussi tourner les anciens binaires à pleine vitesse, et continue de fonctionner quand la génération suivante change toutes les latences. Les cœurs x86 dont le livre attendait qu’ils se heurtent à un mur ont continué d’accélérer, et en 2003 AMD a étendu le x86 à 64 bits : c’est l’« EMT-64 » que mentionne Tanenbaum, adopté par Intel en 2004. Avec un x86 64 bits disponible, le principal argument de l’Itanium disparaissait. Il est resté une niche dans les serveurs haut de gamme, surtout chez HP. Intel a livré ses derniers Itanium en 2021, et Linux a abandonné l’IA-64 en 2024.
Ce qui a survécu
Les idées ne sont pas mortes avec la puce :
- Le VLIW vit dans les DSP et les accélérateurs, où le code est petit, optimisé à la main et tourne sur une seule puce connue : les DSP C6000 de Texas Instruments, et l’Hexagon de Qualcomm, le DSP VLIW de ses puces Snapdragon pour téléphones. Les GPU Radeon d’AMD ont utilisé des conceptions VLIW jusqu’en 2012.
- La prédication est partout, à plus petites doses. Les
cmovetcseldu chapitre précédent sont de la prédication sur une seule instruction. AVX-512 a des registres de masque qui prédiquent chaque voie d’un vecteur, SVE d’ARM a des registres de prédicat dans le même but, et les GPU exécutent les branchements divergents en prédiquant chaque thread. - La spéculation avec fautes différées est devenue une fonction matérielle des cœurs à exécution dans le désordre, qui exécutent au-delà des branchements et annulent ce qui s’avère faux.
- Compiler une fois, traduire ensuite est revenu avec la traduction binaire : les processeurs VLIW de Transmeta exécutaient du code x86 en le traduisant à la volée, comme le fait aujourd’hui Rosetta d’Apple, du x86 vers l’ARM64.
À retenir
- Une ISA VLIW lance des paquets d’opérations que le compilateur garantit indépendantes : matériel simple, code plus gros, binaires liés à une machine.
- EPIC / IA-64 (Itanium, 2001) a ajouté des paquets de 128 bits avec gabarits et groupes d’instructions, 128 registres avec une pile de registres, la prédication complète avec 64 registres de prédicat, et les chargements spéculatifs et anticipés.
- L’ordonnancement à la compilation fonctionne quand le compilateur voit le parallélisme : remontés en tête, trois chargements s’exécutent aussi vite dans l’ordre que dans le désordre (12, 20, 48 cycles).
- Il échoue face au recouvrement de pointeurs, aux branchements et aux latences imprévisibles, que le matériel à exécution dans le désordre traite à l’exécution, pour n’importe quel binaire. Dans l’ordre du source, la machine dans l’ordre a pris 127 cycles contre 48 dans le désordre à la latence du L3.
- Le x86 64 bits d’AMD a retiré à l’Itanium sa raison d’être ; il a été abandonné en 2021. Le VLIW survit dans les DSP, la prédication dans le SIMD et les GPU.