Précédent Remonter Suivant
Page d'accueil de ce livre

Chapitre 9  Au-delà du modèle de von Neumann

Introduction

La recherche de performances a inspiré des tentatives pour contourner la loi d'airain de l'architecture de von Neumann : une seule instruction à la fois. Ces tentatives se classent en deux catégories : radicales et pragmatiques. Le présent chapitre en fait un bref tour d'horizon. Les architectures qui remettent fortement en cause le principe de von Neumann n'existent aujourd'hui que dans quelques laboratoires (même si elles sont sans doute promises à un avenir meilleur et si certaines d'entre elles ont un passé respectable), et celles qui sont des extensions pratiques de ce principe ne remettent pas en cause la sémantique du modèle de calcul, même si elles en compliquent considérablement la mise en oeuvre technique pour le bénéfice d'une amélioration non moins considérable des performances.


9.1  Architectures révolutionnaires

Les tentatives radicales visent à créer de nouveaux modèles de calcul en imaginant des ordinateurs capables d'exécuter un grand nombre d'instructions simultanément. Ces modèles révolutionnaires se classent selon diverses catégories :

9.1.1  SIMD (Single Instruction Multiple Data)

Comme le nom le suggère, il s'agit d'ordinateurs capables d'appliquer à un moment donné la même opération à un ensemble de données, ce qui postule une certaine similarité de celles-ci. Ce postulat est rarement satisfait dans le cas général, mais il s'applique bien à un cas particulier important, celui du calcul vectoriel ou matriciel, qui a de nombreuses applications pratiques en météorologie, en aérodynamique, en calcul de structures, en océanographie, en sismologie, bref dans tous les domaines pour lesquels la physique ne donne pas de solution analytique simple mais où un modèle heuristique étalonné sur des données recueillies pour des problèmes à solution connue et appliqué à un ensemble de données empiriques aussi vaste que possible permet d'atteindre une solution approchée satisfaisante. Le modèle SIMD a connu un certain succès sous le nom d'ordinateur vectoriel.

En réalité un processeur vectoriel ne dispose que d'un seul additionneur (ou respectivement multiplieur, diviseur, etc.), l'exécution des opérations sur les données multiples n'est donc pas exactement simultanée, mais elle repose sur une technique de pipe-line que nous décrirons ci-dessous à la section 9.2.2. La réalisation d'un tel ordinateur pose des problèmes théoriques raisonnablement solubles ; il en va de même pour la programmation.

Les super-ordinateurs Cray, Fujitsu, NEC, les mini-super Convex, les processeurs spécialisés Floating Point Systems ont connu de réels succès jusque dans les années 1980 et 1990 en employant à des degrés divers la technologie vectorielle.

Si la baisse de prix des processeurs ordinaires les a peu à peu éliminées, ces architectures pourraient revenir en scène lorsque les progrès de la technologie classique rencontreront des obstacles sérieux, et s'appliquer assez bien à tous les problèmes de traitement et de création d'images. D'ailleurs le développement extraordinaire des jeux vidéo, qui sont aujourd'hui (en 2002) l'aiguillon le plus aiguisé de la recherche micro-électronique, a nécessité la conception de processeurs graphiques époustouflants qui recyclent pas mal de technologie vectorielle.

9.1.2  Architectures cellulaires et systoliques

Le modèle SIMD poussé à son extrême donne le modèle cellulaire, conçu pour exploiter un parallélisme de données massif : on a toujours un processeur par donnée, les processeurs sont très simples mais ils se comptent par dizaines de milliers. Il y eut surtout des prototypes de laboratoire, excepté le premier modèle de la Connection Machine de l'entreprise modestement nommée Thinking Machines Corporation. Cet ordinateur, doté de 65 536 processeurs, chacun flanqué d'une mémoire locale de 65 536 bits, avait été conçu par Daniel Hillis, un étudiant en intelligence artificielle au MIT et réalisé grâce à une commande du DoD (ministère de la Défense américain). Il s'en vendit une vingtaine d'exemplaires (dont deux en France) pour un coût unitaire équivalent à une dizaine de millions de dollars.

Une variante du modèle cellulaire est l'ordinateur systolique, qui exploite en outre un parallélisme de flux de données : un vecteur de données passe par des processeurs successifs qui correspondent à des étapes successives de calcul et qui effectuent chacun un type d'opération.

Il convient également de mentionner la conception et la réalisation de processeurs systoliques spécialisés pour certaines applications, notamment la comparaison de séquences biologiques. La plupart de ces processeurs, par exemple le réseau systolique linéaire SAMBA de 256 processeurs pour la comparaison de séquences biologiques développé par Dominique Lavenier à l'IRISA de Rennes, implémentent l'algorithme classique de Smith et Waterman (un algorithme dit de « programmation dynamique »).

La comparaison de séquences biologiques est une comparaison inexacte : il s'agit de savoir si deux chaînes de caractères représentant deux séquences d'ADN (l'alphabet est ATGC) ou deux séquences de protéines (l'alphabet est alors plus vaste pour représenter les vingt acides aminés et quelques autres informations) sont suffisamment semblables, au prix de quelques disparités de caractères (de possibles mutations), de quelques décalages (insertions ou délétions).

Le système est constitué de plusieurs processeurs capables surtout de comparer deux caractères entre eux. Les processeurs sont disposés en série ; s'il y en a cent on peut placer dans la mémoire locale de chacun d'eux un caractère d'une séquence de cent caractères que l'on veut étudier en la comparant à une collection de séquences connues par ailleurs. On fait défiler les séquences de la collection (généralement nommée banque) dans les processeurs qui détiennent la séquence étudiée (la cible). Il n'est pas possible d'exposer ici le détail de l'algorithme, mais cette architecture est très bien adaptée à cet algorithme. Malheureusement elle est limitée à un seul algorithme.

9.1.3  MIMD (Multiple Instructions Multiple Data)

Les machines MIMD sont des ordinateurs capables d'exécuter simultanément de nombreuses instructions quelconques appliquées à des données également quelconques. En fait il y a de multiples processeurs qui se partagent une mémoire unique, parfois ils ont chacun en plus une mémoire locale, quelquefois ils n'ont qu'une mémoire locale. L'interconnexion de tous ces processeurs, leur synchronisation et le maintien de la cohérence de la mémoire partagée posent des problèmes absolument passionnants de conception du matériel et du système d'exploitation : une fois ceux-ci résolus il reste à programmer tout ça, et c'est là qu'achoppe l'ambition de ces tentatives. La complexité de la programmation est telle que le temps qu'elle absorbe suffit aux processeurs ordinaires pour combler l'écart de performance que l'architecture MIMD était censée creuser. Ces architectures connaîtront une nouvelle faveur lorsque la courbe inexorable du progrès des processeurs de von Neumann s'infléchira.

Parmi les réalisations MIMD les plus achevées on retiendra les dernières Connection Machines de Thinking Machines Corporation, entreprise disparue durant les cruelles années 1990. Citons aussi les Transputers de la société britannique Inmos, destinés à former les éléments de base d'architectures plus complexes, inspirés des travaux de C.A.R. Hoare1 sur les processus séquentiels communicants. Le Transputer a connu un certain succès, notamment dans le domaine industriel.
Ce rapide tour d'horizon des architectures non von Neumann illustre leur échec général dû à la difficulté de leur programmation. À leur apparition elles ont souvent énormément séduit, mais la nécessité de recourir à des méthodes et à des langages de programmation peu répandus et maîtrisés par peu d'ingénieurs a vite découragé industriels et clients. D'où le succès, en revanche, de contournements du principe de von Neumann par des voies moins radicales, qui recherchaient le même résultat tout en conservant la sémantique classique et que nous allons examiner maintenant.


9.2  Architectures réformistes

Parmi les différentes réformes apportées à l'architecture de von Neumann pour en améliorer les performances tout en conservant un comportement extérieur apparent identique, la plus importante par ses conséquences est peut-être la substitution à la mémoire centrale unique d'une hiérarchie de mémoires de temps d'accès décroissants, depuis le cache de niveau 1 incorporé au processeur jusqu'à la mémoire auxiliaire de pages sur disque : mais cette notion de hiérarchie de mémoire dépasse le cadre du présent chapitre et elle est abordée dans plusieurs chapitres du corps de cet ouvrage. Nous examinerons ici les architectures en « pipe-line » et super-scalaires. Mais auparavant nous devons donner un peu plus de détails sur le déroulement des instructions que nous ne l'avions fait à la sous-section ??.

9.2.1  Séquence d'exécution d'une instruction

Dans l'exposé du principe de l'ordinateur à la sous-section ?? nous avons considéré chaque instruction machine comme une opération indivisible dans le temps, atomique, et il en est bien ainsi selon von Neumann. En fait la plupart des instructions sont effectuées selon une séquence temporelle identique et régulière dont nous allons exposer un exemple.

L'exécution des instructions successives de notre architecture a lieu selon la séquence suivante. Notons que cet exemple, pour une raison que nous allons voir, postule des instructions à format fixe.
  1. Étape de lecture FETCH : l'unité de contrôle va chercher en mémoire la prochaine instruction à exécuter. Comment sait-on où aller la chercher ? L'unité de contrôle maintient cette information dans un compteur de programme (PC) qui à chaque instant contient l'adresse en mémoire de l'instruction suivante. Dans le cas le plus simple (pas de débranchement) c'est l'adresse du mot qui suit l'instruction en cours. Dans le cas d'un débranchement : eh bien l'instruction de débranchement consiste précisément à placer dans le PC l'adresse de l'instruction à laquelle le programme doit se poursuivre (doit « sauter » : les débranchements sont aussi appelés sauts ; on distingue les branchements conditionnels et les sauts simples, inconditionnels).

    Le PC peut résider dans un registre de l'unité centrale. Dans notre exemple simple il serait possible de lui réserver R qui n'est pas utilisé à autre chose. Dans beaucoup de processeurs le PC est une partie du PSW (Program Status Word) qui conserve différents indicateurs fondamentaux du processeur.

  2. Étape de décodage DEC : l'unité de contrôle analyse le code opération (5 premiers bits dans notre exemple de la section ??) et détermine ainsi le circuit logique qui correspond à l'instruction désirée. Simultanément au décodage, l'unité de contrôle effectue la lecture des registres impliqués dans l'instruction. Cette simultanéité impose des instructions à format fixe, où le nombre et l'emplacement des bits qui désignent les registres soient toujours les mêmes. Le contenu des registres est recopié dans des registres de travail temporaires.

  3. Étape d'exécution EXEC : l'instruction déterminée à l'étape de décodage est exécutée ; s'il doit y avoir un accès mémoire l'adresse effective est calculée ; s'il s'agit d'un branchement conditionnel le contenu du registre lu à l'étape précédente permet de déterminer si le branchement doit être « pris » et si oui l'adresse de branchement est calculée.

  4. Étape d'accès mémoire MEM : cette étape a lieu pour les opérations de chargement et de rangement en mémoire et pour les branchements. Les chargement ou rangements ont lieu, ou la valeur convenable est placée dans le PC. Ces opérations ont lieu dans le cache, ce qui explique que cette étape ait une durée comparable aux autres.

  5. Étape d'écriture du résultat RES dans les registres affectés.

9.2.2  Le pipe-line

Principe du pipe-line

Pour l'exemple de processeur que nous venons de donner, l'exécution d'une instruction se décompose en cinq étapes plus élémentaires. À part l'exécution proprement dite (étape EXEC), ces étapes sont les mêmes quelle que soit l'instruction particulière qui doit être exécutée. L'idée est venue de confier chaque étape à une unité de circuits logiques spécifique : unité de lecture, unité de décodage, unité d'exécution, unité d'accès mémoire, unité de livraison du résultat. Ainsi le processeur peut commencer à traiter une instruction avant que la précédente soit terminée, en utilisant les unités libérées par la terminaison des premières étapes. Soient i1,i2,... i6 six instructions consécutives, elles seront traitées ainsi :
i1 FETCH DEC EXEC MEM RES          
i2   FETCH DEC EXEC MEM RES        
i3     FETCH DEC EXEC MEM RES      
i4       FETCH DEC EXEC MEM RES    
i5         FETCH DEC EXEC MEM RES  
i6           FETCH DEC EXEC MEM RES

Cette structure s'appelle un pipe-line, parce qu'elle évoque un tuyau dans lequel les instructions s'engouffrent les unes derrière les autres sans attendre que la précédente soit sortie. Nos instructions sont découpées en cinq étapes, ce qui fait que notre pipe-line à un moment donné contient cinq instructions en cours d'exécution : on dit que c'est un pipe-line à cinq étages. Certains processeurs ont des pipe-lines avec sept ou huit étages, voire plus : le Pentium III a douze étages de pipe-line, le Pentium IV en a vingt.

Cycle de processeur, fréquence d'horloge

Qu'est-ce qu'un cycle de processeur ? Toutes les opérations dans un processeur sont synchronisées par une horloge, ce qui permet de savoir à quel moment le résultat d'une micro--opération va être disponible. J'appelle micro--opération une opération moins complexe qu'une instruction, par exemple une étape d'instruction telle que décrite à la section 9.2.1. Il est nécessaire de savoir à quel instant précis telle modification de tel registre sera disponible et stable, pour pouvoir enchaîner la micro--opération suivante : c'est le rôle de l'horloge.

Comment fonctionne l'horloge du processeur ? Elle repose sur un dispositif à quartz, qui régule un circuit oscillant selon une fréquence extrêmement précise. À chaque fin de période le circuit oscillant émet un signal. On rappelle que la période est l'inverse de la fréquence : si un circuit a une période de 1/50 de seconde, on dit que sa fréquence est de 50 Herz (Hz). La sortie du circuit logique correspondant à une micro--opération est couplée à une entrée d'un circuit ET dont l'autre entrée est couplée à l'horloge. À chaque top d'horloge la porte ET délivre un résultat. Ainsi toutes les opérations sont synchronisées. Ce que l'on appelle la fréquence d'un processeur est la fréquence de son horloge.

Processeurs asynchrones

Ce cadencement des opérations simplifie grandement la conception des circuits, mais on peut imaginer qu'il fait perdre du temps à certains endroits : toutes les micro-opérations ne durent pas le même temps et certains résultats obtenus ne sont pas disponibles parce qu'on attend le top d'horloge. Aussi existe-t-il un domaine de recherche prometteur, les processeurs asynchrones. La synchronisation est remplacée par la signalisation : un élément de circuit qui obtient un résultat prévient le consommateur de ce résultat par l'émission d'un signal, ce qui nous place dans une logique assez semblable à celle des protocoles de réseau, où l'on ignore toujours ce qui se passe « à l'autre bout ». Parmi les avantages collatéraux fournis par l'asynchronisme, signalons une consommation électrique et des interférences électromagnétiques réduites.

Comme beaucoup de techniques avancées, les processeurs asynchrones sont aujourd'hui un domaine assez confidentiel, mais qui pourrait connaître une grande expansion au fur et à mesure que les progrès des processeurs classiques deviendront plus laborieux.

Apport de performances par le pipe-line

Le pipe-line est un facteur considérable d'accélération des processeurs, et notamment d'augmentation de la fréquence nominale.

Supposons une architecture classique où le temps total d'exécution d'une instruction simple telle qu'un chargement de registre ou une addition registre à registre correspond à un ou deux cycles (même si des instructions complexes peuvent prendre dix ou vingt cycles).

Prenons maintenant une architecture avec un pipe-line à cinq étages, comme dans notre exemple, et avec des éléments micro--électroniques aussi rapides que notre architecture classique hypothétique. Notre instruction va prendre le même temps pour s'exécuter, mais elle est maintenant cadencée par le passage dans les cinq étages du pipe-line. Chaque étage correspond à un cycle. L'instruction qui s'exécutait en deux cycles s'exécute maintenant en cinq cycles, toujours dans le même temps : c'est que chaque cycle est plus court. La fréquence a été multipliée par 2,5. C'est ainsi que le pipe-line est l'arme secrète qui permet les fréquences élevées des processeurs contemporains.

Certes, pour une instruction donnée chaque cycle « fait » moins de choses, mais ce n'est pas une escroquerie : à chaque fin de cycle il y a bien livraison du résultat d'une instruction. Enfin presque. Parce qu'il y a quand même des difficultés qui s'opposent parfois au fonctionnement continu du pipe-line.

Limite du pipe-line : les branchements

La première difficulté inhérente au pipe-line est de bon sens : on commence à exécuter plusieurs instructions consécutives, mais s'il y a un débranchement au milieu l'ordre d'exécution sera modifié. Notre processeur aura commencé à exécuter des instructions dont la suite des événements montre que ce n'était pas les bonnes.

Quand sait-on si un branchement conditionnel a effectivement lieu ? Lors de l'étape EXEC. Quand est-il exécuté ? Lors de l'étape MEM.

Si par exemple i3 est un branchement conditionnel et que le chemin choisi soit effectivement le débranchement, cette condition est connue lors de l'étape EXEC de i3. À ce stade, i4 est déjà passée par les étapes FETCH et DEC, i5 par l'étape FETCH. Rien d'irrémédiable n'a été accompli, ni accès mémoire ni écriture de résultat, mais on a perdu du temps, en exécutant des étapes pour des instructions inutiles et il faut « vider » la suite du pipe-line pour le recharger avec d'autres instructions.

Il est assez clair que si les débranchements sont fréquents le gain de performances procuré par le pipe-line va fortement diminuer. Plus le pipe-line a d'étages plus la pénalité sera forte, et dans certains cas il sera nécessaire de restituer à certains registres leur valeur antérieure, modifiée par une exécution trop hardiment anticipatrice. Le processeur devra comporter des circuits logiques destinés à traiter ces situations de retour en arrière. On le voit, tout gain de performance a un coût qui en atténue l'avantage.

Les architectes de processeurs déploient des techniques très subtiles pour éviter l'occurrence trop fréquente de ces situations de débranchement (de saut) où le pipe-line « cale » (to stall). La prédiction de branchement consiste à essayer de savoir à l'avance, par l'examen anticipé du texte du programme, si un branchement va avoir lieu, et dans ce cas modifier l'ordre des instructions pour annuler le branchement. Dans le cas d'une section de programme répétitive, l'historique des exécutions permet de savoir quels sont les branchements les plus probables et de réorganiser le texte du programme pour que le cas le plus fréquent se déroule sans branchements. En désespoir de cause on peut recourir à la technique du « branchement retardé » qui consiste à insérer une instruction nulle derrière le branchement, ce qui évite d'avoir à commencer des instructions qu'il faudra annuler : ceci n'est jamais nécessaire dans notre exemple de pipe-line à cinq étages, mais peut l'être avec un plus grand nombre d'étages ; dans ce cas la détection de branchement pourrait intervenir après que les instructions suivantes auront modifié des registres, qu'il faudrait alors restaurer à leur valeur précédente, ce qui serait extrêmement complexe.

Les auteurs de compilateurs sont aussi mis à contribution dans la lutte contre les branchements. Ils sont invités à produire du code machine aussi exempt de branchements que possibles, à anticiper les cas les plus probables pour leur réserver l'exécution la plus linéaire possible. Évidemment ces efforts ne réussissent pas toujours et il reste des branchements à détecter dynamiquement, c'est-à-dire lors de l'exécution (par opposition à une détection statique, par l'examen du texte du programme).

Limite du pipe-line : les interruptions

La seconde difficulté de réalisation d'un pipe-line efficace est d'une nature voisine de celle que nous venons d'examiner, mais encore plus redoutable. Nous avons bien des difficultés avec les débranchements, mais au moins ce sont des événements internes à un programme, à un processus et à un processeur. Mais qu'en est-il lorsque survient un événement asynchrone, dont le type par excellence est l'interruption ?

Dans une architecture traditionnelle, von-Neumannienne de stricte obédience, le processeur à un instant donné exécute au plus une instruction. La valeur du compteur ordinal (PC, eip...) désigne exactement la limite entre ce qui est déjà exécuté et ce qui reste à exécuter. C'est en général après la terminaison de chaque instruction que le processeur scrute les registres de contrôle des interruptions pour savoir s'il y en a une en attente, auquel cas il décide de la traiter, ou de ne pas la traiter d'ailleurs si les interruptions sont masquées à ce moment. Le cas des interruptions pour faute de page est un exemple de situation un peu différente, où le PC après l'interruption doit pointer sur l'instruction qui a causé la faute afin qu'elle puisse être exécutée à nouveau, la page étant désormais en mémoire. Le problème est assez bien circonscrit.

Avec une architecture à pipe-line, l'interruption survient dans un contexte où plusieurs instructions sont en cours d'exécution à des stades variés. La valeur du compteur ordinal ne rend pas forcément fidèlement compte de la limite entre ce qui est exécuté et ce qui ne l'est pas. Sans doute, elle donne l'adresse de la prochaine instruction à introduire dans le pipe-line, plus probablement que celle de la dernière instruction exécutée. Bref, après avoir traité l'interruption le processeur devra déterminer où il s'était arrêté dans le pipe-line et trouver un état bien déterminé pour redémarrer, ce qui ne sera pas simple.

Nous n'entrerons pas dans les détails de ce problème, qui relève de l'architecture matérielle plus que du système d'exploitation, mais sachons que pour mieux le cerner les processeurs modernes ont souvent deux types d'interruptions : les interruptions précises qui laissent le processeur dans un état bien défini (PC sauvegardé en un endroit connu, instructions antérieures à celle pointée par le PC totalement exécutées, instructions ultérieures non entamées, état de l'instruction courante connu), et les interruptions imprécises qui laissent tout en chantier, charge au programmeur de système de reconstituer une situation de redémarrage cohérente à partir des informations d'état que le processeur aura obligeamment déversées sur la pile. Il y a des cas où une interruption imprécise fait très bien l'affaire, par exemple si elle correspond à une erreur fatale l'état ultérieur du programme importe peu.

Pour prendre un exemple, l'architecture ARC 700 lancée en 2004 comporte un modèle d'interruptions précises qui permet, au choix, de forcer la terminaison de toutes les instructions qui précèdent celle qui a provoqué l'exception ; d'annuler l'instruction fautive avant de valider son résultat ; d'avertir la routine de traitement des exceptions de la cause de l'exception ; et de relancer le pipe-line après le traitement de l'exception dans l'état où il se trouvait auparavant.

Les interruptions précises augmentent considérablement la complexité des circuits matériels dévolus au contrôle des interruptions, et de ce fait les architectes essayent de les éviter. Les interruptions imprécises sont de plus en plus populaires parmi les architectes de processeurs, et comme d'habitude la complexité est renvoyée aux concepteurs de systèmes d'exploitation, ce qui est après tout de bonne politique : il est plus facile de modifier un paragraphe de programme qu'un morceau de silicium (déjà installé dans le salon du client de surcroît).

Lorsque nous aborderons le modèle d'exécution super-scalaire quelques pages plus bas, nous verrons que les difficultés causées par un contexte mal déterminé lors d'une interruption sont encore plus graves.



9.2.3  RISC, CISC et pipe-line

L'idée du pipe-line est apparue assez tôt ; l'IBM 7030 Stretch (1960), une machine à mots de 64 bits, est considéré comme la première machine à pipe-line. Le Stretch succédait au 704 et devait être 100 fois plus puissant. Il n'en a été construit que seize exemplaires 2 mais cette machine a représenté une étape importante avec beaucoup d'innovations.

Le Control Data 6600, une autre machine novatrice apparue en 1964, considérée comme le premier super-ordinateur et dont l'architecte principal était Seymour Cray, comportait aussi une structure de pipe-line, et ses concepteurs avaient compris que pour qu'elle fonctionne efficacement il fallait des instructions simples, de longueur fixe, toutes à peu près de la même durée d'exécution, si possible en un cycle. Seymour Cray poussera ces principes à l'extrême dans les super-ordinateurs vectoriels qu'il construira sous son nom pour Cray Research, et qui furent les plus puissants de leur époque.

Cette technique du pipe-line et l'évolution consécutive des techniques de compilation se sont beaucoup développées avec l'apparition à la fin des années 1980 des processeurs à architecture RISC (Reduced Instruction Set Computer), par opposition à la mode précédente rétrospectivement baptisée CISC (Complex Instruction Set Computer).

Les machines CISC qui ont connu leur apogée au début des années 1980 (VAX de Digital Equipment Corporation, 68000 de Motorola) avaient un jeu d'instructions très vaste (plus de 300 pour le VAX) et très complexe, avec des instructions de longueurs différentes, et même parfois de longueur variable selon les opérandes. La richesse du jeu d'instructions était censée faciliter la tâche des programmeurs qui utilisaient le langage assembleur et, surtout, des auteurs de compilateurs pour langages évolué, en leur fournissant des instructions machine qui ressemblaient déjà à du langage évolué, plus facile et maniable pour le programmeur. D'ailleurs le langage C, langage évolué de bas niveau (on a pu le qualifier d'assembleur portable, la notion de portabilité désignant l'aptitude à fonctionner sur des ordinateurs d'architectures différentes), est assez largement inspiré de l'assembleur des ordinateurs PDP, ancêtres des VAX.

Cette richesse et cette complexité du jeu d'instructions avaient bien sûr un coût en termes de complexité et de lenteur du processeur. Le risque était notamment que les instructions simples (chargement de registre depuis la mémoire, copie de registre vers la mémoire, addition registre à registre) soient condamnées à s'exécuter aussi lentement que les opérations complexes (copie de zone mémoire à zone mémoire de longueur variable, opérations complexes en mémoire). Le VAX notamment n'esquivait guère ce risque.

Dans la seconde moitié des années 1970 des chercheurs ont fait des statistiques sur la composition en instructions des programmes en langage machine, soit qu'ils aient été directement écrits en assembleur, soit qu'ils aient été produits par un compilateur. Citons notamment les travaux de D.A. Fairclough, (A unique microprocessor instruction set, IEEE Micro, mai 1982 [24]). Ils constatèrent d'abord que 43% des instructions étaient des déplacements de données d'un endroit à un autre, que le quart des instructions étaient des branchements, ensuite que pour chaque programme le nombre d'instructions utilisées était très réduit, enfin que seules les instructions les plus simples étaient largement utilisées. Une autre constatation était que les opérations de loin les plus coûteuses étaient l'appel de sous-programme (un programme lance l'exécution d'un autre programme en lui communiquant des paramètres) et le retour d'un sous-programme au programme principal.

Sur la base de ces constatations ils préconisèrent de concevoir des processeurs avec un jeu réduit d'instructions plus simples. La notion de processeur RISC était née, et le premier processeur de ce type, l'IBM 801, fut réalisé par John Cocke en 1979. La société MIPS, fondée par John Hennessy, pionnier du RISC à l'Université Stanford, fut créée en 1985. Hewlett-Packard fut le premier grand constructeur d'ordinateurs à réaliser toute sa gamme en architecture RISC en 1986. Sun et Digital Equipment suivirent. Le livre emblématique de la révolution RISC, Computer architecture: a quantitative approach [28], a été écrit par John L. Hennessy, l'architecte des processeurs MIPS, et David A. Patterson, originaire du campus de Berkeley, l'architecte des processeurs SPARC de Sun. Les processeurs MIPS ont été les premiers à défricher la voie, et les plus hardiment innovateurs : nous l'avons vu au chapitre 4 à propos de l'utilisation du TLB. Il faudrait ajouter à ce répertoire Richard L. Sites, l'architecte des processeurs Alpha de Digital.

Les traits les plus frappants initialement dans les architectures RISC étaient le petit nombre d'instructions, avec l'absence d'instructions mémoire--mémoire : il n'y avait que des chargements de mémoire dans un registre, des copies de registre vers la mémoire et des opérations dans les registres.

D'autres traits se révélèrent bientôt aussi importants : longueur et format d'instruction fixes, usage intensif du pipe-line. Pour tirer parti correctement d'une architecture aussi ascétique une grande part de la complexité était reportée sur les compilateurs, qui devaient produire un code capable d'utiliser efficacement les registres et le pipe-line.

La nouvelle architecture se révéla bientôt extrêmement rapide, et doublement rapide : en effet, pour tirer parti d'un progrès de la micro-électronique, il ne suffit pas de concevoir un processeur rapide, il faut aussi que le délai nécessaire à sa conception ne soit pas trop long. La simplicité du RISC était aussi un atout de ce côté. Actuellement il faut à peu près trois ans à une équipe de 100 à 200 ingénieurs pour concevoir un nouveau processeur. Une usine entièrement équipée pour le construire coûtera de l'ordre de quatre milliards de dollars. La conception d'une architecture novatrice demande une douzaine d'années.

La technologie CISC parut condamnée, ce qui fut effectivement le cas pour les gammes VAX et Motorola 68000. Tout le monde attendait la chute de l'architecture x86 d'Intel, sur laquelle reposent les dizaines de millions de PC vendus chaque année. C'était compter sans les efforts qu'Intel pouvait mobiliser grâce à la rente du PC. Les Pentium actuels sont en fait des processeurs constitués d'un noyau RISC autour duquel des circuits supplémentaires et du micro-code (notion introduite ci-dessous à la section 9.2.4) simulent l'ancienne architecture CISC afin de préserver la compatibilité avec les systèmes et les programmes existants.

Quant à la gamme des grands systèmes IBM, l'immense stock de programmes existants dont la conversion exigerait des dépenses phénoménales semble la vouer à une immortalité dont l'érosion ne se fait qu'au gré du changement lent des applications.

Même si l'évolution actuelle suggère un demi-échec de l'architecture RISC dont la remplaçante VLIW (Very Long Instruction Word) entre en scène avec l'architecture IA-64 conçue conjointement par Intel et Hewlett-Packard, tous les processeurs modernes ont intégré sa leçon. Le mouvement RISC a profondément révolutionné la conception des processeurs et aussi, de façon moins spectaculaire mais aussi profonde, celle des techniques de compilation. En fait, hommage du vice à la vertu, tous les processeurs modernes comportent un coeur RISC entouré de circuits qui procurent un décor différent, Pentium par exemple. Même les grands systèmes IBM sont maintenant animés par des microprocesseurs qui implémentent leur jeu d'instructions traditionnel en RISC.

9.2.4  Micro-code : le retour

Le micro-code était un élément architectural intermédiaire entre le logiciel et le matériel, caractéristique des processeurs CISC, à peu près disparu sur les processeurs RISC. En fait il s'agissait d'une couche de logiciel de très bas niveau qui simulait certains éléments de matériel pour les présenter au logiciel. Ainsi l'architecture 360 comportait 16 registres généraux, mais les modèles les plus petits comme le 360/20 n'en avaient que deux (un accumulateur et un registre). La couche de micro-code, stockée dans une mémoire spéciale, présentait au système d'exploitation une machine virtuelle à 16 registres, implantée sur la machine réelle à deux registres. Une disquette permettait de charger en mémoire une nouvelle version du micro-code. Le BIOS des PCs Intel joue un peu le même rôle, il sert notamment à présenter les périphériques au système sous un aspect différent de leur réalité physique, ce qui permet par exemple de voir tous les disques de la même façon, avec un adressage linéaire des blocs de données, quelle que soit leur structure réelle. Le micro-code était un facteur de complexité et de lenteur, ce pourquoi la révolution RISC en a fait table rase. Mais ne survit-il pas en fait à l'intérieur du processeur ? Si bien sûr : les processeurs modernes sont si complexes et embarquent tellement de mémoire sur le chip (la puce proprement dite) que leur réalisation est elle-même micro-programmée. Le processeur Intel Pentium, de la famille CISC, est construit autour d'un coeur RISC entouré de micro-code qui simule l'architecture officielle. Le micro-code est aussi présent dans les contrôleurs de disques et autres périphériques. À l'heure où il a officiellement disparu, il n'a jamais autant existé.

9.2.5  Super-scalaire

La décomposition des instructions en étapes plus élémentaires que nous avons examinée à la section 9.2.1 permet le pipe-line, mais aussi une autre forme de simultanéité que l'on appelle « super-scalaire », et qui consiste à avoir plusieurs unités d'exécution actives simultanément, selon le modèle de la figure 9.1. Ce modèle d'exécution se combine avec le pipe-line dans les architectures modernes.


Figure 9.1 : Modèle de l'exécution super-scalaire.


Les instructions sont décodées à l'avance et emmagasinées dans le buffer, qui est en fait un jeu de registres. Dès qu'une unité d'exécution se libère, une instruction est extraite du buffer pour être exécutée. L'architecture super-scalaire de la figure 9.1 peut exécuter quatre instructions par cycle. Si de plus le pipe-line a divisé le temps de cycle par trois, la combinaison de ces deux techniques a multiplié la vitesse de notre processeur par 12 à technologie micro-électronique constante. Évidemment, ceci est vrai en l'absence de dépendances entre les données manipulées par les instructions successives, parce que si le pipe-line introduisait des problèmes en cas de branchement, le traitement super-scalaire introduit des risques de collisions de données. Supposons une machine à seize registres, deux unités d'exécution et le programme suivant :
Buffer Unité Unité
d'ordre   d'exécution 1 d'exécution 2
1 R2 + R3 ® R4    
2 R5 - R6 ® R7    
3 LOAD R8, COUNT    
4 R8 * R2 ® R3    
    R2 + R3 ® R4 R5 - R6 ® R7
  conflit de données ® LOAD R8, COUNT  
  potentiel évité ici R8 * R2 ® R3  

Le conflit de données est le suivant : l'instruction 3 charge le registre R8 avec la valeur contenue à l'adresse mémoire COUNT. L'instruction 4 effectue la multiplication des contenus des registres R8 et R2 et écrit le résultat dans R3. L'écriture rigoureusement fidèle à von Neumann de notre programme stipule une exécution séquentielle, l'instruction 4 suit l'instruction 3 et utilise dans R8 la valeur que cette dernière y aura déposée.

Si nous lançons l'instruction 3 dans l'unité d'exécution 1 et, simultanément, l'instruction 4 dans l'unité d'exécution 2, quelle sera la valeur du contenu de R8 utilisée par l'instruction 4 ? Celle qui résulte de l'exécution de l'instruction 3, ou celle qu'il contenait auparavant ? La réponse à cette question est indéterminée, et pour maintenir la sémantique d'une machine de von Neumann le processeur doit contenir une logique capable de détecter ce problème et de décider de ne pas exécuter ces deux instructions simultanément, ce qu'illustre notre figure. Cette détection doit avoir lieu dynamiquement, à la volée, pour chaque exécution de cette séquence d'instructions.

L'évitement des conflits de données complique le circuit et diminue le bénéfice attendu de l'architecture super-scalaire. Ce bénéfice reste néanmoins suffisant pour que tous les processeurs modernes utilisent ces techniques, même au prix d'une complexité proprement diabolique. La section suivante nous montrera comment les architectes de processeurs se sont débarrassés de cette complexité et ont passé la patate chaude aux auteurs de compilateurs.

Bien évidemment, les difficultés que nous avons signalées à la section 9.2.2 au sujet du pipe-line et qui procèdent de la survenue d'événements asynchrones (les interruptions) dans un contexte où plusieurs instructions sont en cours d'exécution à des stades variés d'achèvement, ces difficultés se retrouvent encore aggravées par l'exécution super-scalaire, puisque ce modèle respecte encore moins l'ordre d'exécution que le pipe-line.



9.2.6  Architecture VLIW (Very Long Instruction Word)

Les modèles d'exécution en pipe-line et super-scalaire ont l'avantage considérable sur les architectures SIMD ou MIMD de préserver la sémantique de l'architecture de von Neumann, ce qui permet de continuer à utiliser les langages de programmation traditionnels avec les méthodes et les compétences qui existent et qui sont éprouvées. Le prix à payer pour cet avantage, c'est d'avoir à faire de la prédiction de branchement, de la prévention de conflits de données et du traitement d'interruptions imprécises, et nous avons vu que ce n'était pas précisément simple.

L'espoir forcément est né de bénéficier des avantages sans avoir à supporter les inconvénients. La différence entre le monde de la technique et celui des relations humaines, c'est que dans le premier une telle idée est vertueuse.

Pour faire fonctionner pipe-line et multiples unités d'exécution sans arrêts brutaux, il suffirait que le programme (ou plutôt le compilateur qui traduit le programme en langage machine) « dise » au processeur quelles sont les instructions susceptibles d'être exécutées simultanément sans risque de conflit de branchement ou de données, et que le processeur soit équipé d'un format d'instruction capable de recevoir ces informations.

Les ingénieurs de Hewlett-Packard et d'Intel ont réuni leurs efforts pour concevoir une telle architecture, connue sous le nom propre IA-64, qui est une architecture VLIW (Very Long Instruction Word). La conception de IA-64 a pris une douzaine d'années avant de déboucher en 2001 sur la livraison d'un premier processeur, Itanium.

Parallélisme explicite

La méthode mise en oeuvre par IA-64 s'appelle EPIC (Explicitly Parallel Instruction Computing). Le processeur reçoit du compilateur une liasse (bundle) de 128 bits. Chaque liasse comporte trois instructions de 41 bits et un masque (template) de 5 bits. Chaque instruction comporte trois numéros de registre de sept bits chacun (ce qui autorise 27=128 registres), six bits de registre de prédicat (predicate register) et un code opération de 13 bits.
Liasse (bundle) IA-64
Instruction Instruction Instruction Masque (5 bits)
(41 bits) (41 bits) (41 bits) (template)

Instruction IA-64
code n° registre prédicat n° registre 1 n° registre 2 n° registre 3
opération (predicate register)      

Les cinq bits de masque indiquent quelles sont les instructions qui peuvent s'exécuter en parallèle, ainsi que l'éventualité du chaînage de cette liasse avec une autre liasse qui en contiendrait la suite. Les compilateurs peuvent placer dans ce masque des valeurs qui indiquent au processeur les instructions à lancer en parallèle3.

Confier au compilateur tout ce travail auparavant réalisé par le processeur présente plusieurs avantages. Le processeur ne possède aucune information a priori sur le programme qu'il exécute, il ne peut que les déduire du texte binaire au fur et à mesure qu'il en décode les instructions, tandis que l'auteur du compilateur peut se conformer à une stratégie systématique d'organisation des instructions.

De plus, le compilateur peut passer beaucoup de temps à optimiser le code, en principe le programme n'est compilé que par son auteur, ou par celui qui l'installe sur un ordinateur, alors qu'il sera exécuté par de nombreux utilisateurs. Il est efficace de consacrer du temps à la compilation pour en gagner à l'exécution.

Élimination de branchements

Imaginons le fragment de programme suivant en pseudo--langage évolué :
Si I égale 0
   Alors
      instruction 1
   Sinon
      instruction 2
que le compilateur traduirait en pseudo--assembleur pour un processeur RISC classique (non--EPIC) :
       COMPARE I à 0
       SAUTE   à l'étiquette SINON si non-égal
ALORS: instruction 1
       SAUTE   à l'étiquette SUITE
SINON: instruction 2
SUITE:
Voici comment procédera un processeur EPIC :
COMPARE I à 0
commence à décoder instruction 1, 
     le prédicat positionné pour pointer sur le registre de
     prédiction P1
commence à décoder instruction 2, 
     le prédicat positionné pour pointer sur le registre de
     prédiction P2
si I égale 0, positionner le registre P1 à vrai (1),
              positionner le registre P2 à faux (0)
calculer et délivrer les résultats de toutes les 
     instructions dont le prédicat pointe sur le 
     registre dont la valeur est vrai (1), en 
     l'occurrence P1.
Le processeur n'exécute aucun saut (débranchement), il commence à exécuter simultanément les branches ALORS et SINON comme s'il s'agissait d'instructions en séquence.

IA-64 prévoit 64 registres de prédicat susceptibles de recevoir les valeurs vrai ou faux (0 ou 1). Le champ registre de prédicat de chaque instruction pointe sur un registre de prédicat. Quand le processeur a fini de comparer I à 0, seuls les résultats des instructions qui pointent sur un registre de prédicat qui vaut vrai (1) sont délivrés.

Considérons ce qui se passe : d'abord, il n'y a aucun risque à commencer l'exécution simultanée des branches ALORS et SINON de ce programme, il n'y a par construction aucune dépendance entre elles puisqu'elles sont exclusives l'une de l'autre. Ensuite nous pouvons observer que notre pseudo--code EPIC ne comporte plus de branchement : ce n'est pas un tour de passe-passe, cette suppression de la plupart des branchements est un fondement du modèle EPIC.

Mais, pourra objecter le lecteur vigilant, en quoi cette suppression des branchements peut-elle être bénéfique pour les performances ? Après tout le processeur EPIC, en commençant à exécuter les deux branches du programme, effectue deux fois plus de travail qu'un processeur classique qui essaie de prévoir quelle branche va être exécutée réellement (et il faut savoir que les processeurs modernes sont capables de faire une prédiction juste dans 90% des cas), et la moitié de ce travail est inutile, puisque finalement seule une des branches sera retenue.

= .9@percentPremier temps de la réponse à l'objection : d'abord exécuter les deux branches en parallèle ne fait pas perdre de temps, puisqu'elles sont indépendantes l'une de l'autre et que notre processeur dispose de tous les bons dispositifs super-scalaires, notamment des unités d'exécution multiples, pour que le parallélisme soit effectif. Ensuite, pour les 10% de cas dans lesquels le processeur classique prédit la mauvaise branche le gain est considérable, puisqu'alors le processeur classique doit tout reprendre au départ. Enfin puisque le texte du programme ne contient plus de branchements il n'est plus divisé en petits blocs ALORS/SINON, ce qui autorise les instructions des branches à être placées dans des liasses ou des chaînes de liasses, associées aux instructions précédentes ou suivantes. Associées signifie ici éligibles pour le parallélisme explicite. = .15@percent

Optimisation des accès mémoire : chargement anticipé

Dans le modèle VLIW, comme dans le modèle RISC, les opérandes d'une instruction doivent être chargés dans des registres avant d'être traités. L'exécution efficace de l'instruction LOAD est donc un élément crucial de l'architecture. Pour en comprendre les ressorts nous invitons le lecteur à se remettre tout d'abord en mémoire ce qui a été écrit sur la technique du cache à la section 4.6.1.

Les développements sur le cache ont montré l'extrême importance de sa bonne gestion. Si par malheur au moment où le processeur essayait d'exécuter une instruction LOAD la zone mémoire recherchée n'était pas dans le cache il en résulterait une pénalité de l'ordre de 20 cycles au moins, autant dire que le fruit de tous nos efforts de parallélisme et de pipe-lining serait anéanti, et au-delà. Il est donc crucial d'éviter de telles situations. Pour ce faire un processeur moderne (EPIC comme l'Itanium, mais aussi RISC comme l'Alpha ou CISC comme l'AMD Athlon) aura recours au chargement anticipé dans le cache L1 des zones mémoires qui seront nécessaires aux instructions appelées à s'exécuter dans les quelques dizaines de cycles qui suivent. C'est le chargement spéculatif.



De la programmation VLIW

Arrivé ici, le lecteur sera en droit de penser que les notions qu'il pouvait avoir de la programmation des ordinateurs viennent de recevoir un sacré surcroît de complexité, et il n'aura pas tort. Il pourra alors se dire que cette activité qui lui semblait banale prend enfin des couleurs attrayantes, ou au contraire que c'est trop effrayant et qu'il renonce à son projet de s'y engager.

En fait, ce qui se complique, c'est la programmation en langage machine ou en assembleur. Aujourd'hui pratiquement personne ne programme plus en langage machine, et quand on le fait ce sont des programmes très brefs, comme une séquence d'amorçage (boot). Quant à la programmation en assembleur, elle est réservée aux auteurs de systèmes d'exploitation et aux auteurs de compilateurs. L'immense majorité des programmes sont écrits de nos jours en langage évolué, et sont traduits en assembleur, puis en langage machine, par un compilateur. Beaucoup de programmes embarqués (à bord d'avions, de voitures, de fours à micro-ondes, de téléphones portables) étaient encore il y a peu écrits en assembleur pour des raisons de performance et d'encombrement : les progrès de la micro-électronique permettent aujourd'hui de les écrire dans des langages hyper-évolués tels que Java ou Ada. Certains compilateurs ne sont pas écrits en assembleur et ne produisent pas directement de l'assembleur : il traduisent le langage source auquel ils sont destinés vers un langage cible de plus bas niveau, qui dispose lui-même d'un compilateur vers l'assembleur, appelé compilateur en mode natif.

Pour nous résumer, le lecteur qui se sent irrésistiblement attiré par les subtilités de la programmation en assembleur pour une architecture VLIW doit se tourner vers l'écriture de compilateurs ou de systèmes d'exploitation. Il n'aura pas à le regretter, ce sont des domaines passionnants, en évolution rapide et où le chômage ne menace pas. Le lecteur que la complexité de ces techniques rebuterait peut être rassuré : l'auteur de programmes en langage évolué n'a pas à se préoccuper de détails techniques d'aussi bas niveau, les langages modernes procurent tous à leurs programmeurs un niveau d'abstraction qui leur permet de se consacrer au problème à programmer plutôt qu'aux détails techniques de l'ordinateur.


1
Le Britannique C. Antony R. Hoare est à l'origine de quelques contributions de premier plan. Des études universitaires de tonalité plutôt littéraire lui ont donné l'occasion de partir en stage en 1960 dans le laboratoire de Kolmogorov à l'Université de Moscou pour travailler sur un projet de traduction automatique. Pour réaliser un dictionnaire électronique nécessaire à ce projet (par ailleurs vite abandonné) il inventa l'algorithme de tri « Quicksort » que tout étudiant en informatique se doit d'avoir étudié en détail et programmé. Ce stage lui avait donné le goût de la programmation, mais, comme il le raconte lui-même avec humour, il eut la chance que sa formation initiale lui fermât les portes du UK National Physical Laboratory, et il entra dans l'industrie. C'est là qu'il eut l'occasion de participer au développement de systèmes d'exploitation, domaine pour lequel il élabora la méthode des moniteurs de Hoare afin de contrôler les accès concurrents et exclusifs à des ressources partagées et la théorie des processus séquentiels communicants, encore aujourd'hui le seul modèle complet et cohérent qui excède réellement le modèle de von Neumann.
2
La console de l'un d'eux, acquis par le CEA, figure dans les réserves du Musée National des Techniques au Conservatoire National des Arts et Métiers, d'où elle est parfois extraite pour des expositions temporaires.
3
Pour ce faire, le processeur Itanium, premier de l'architecture IA-64, possède deux unités d'exécution d'instructions arithmétiques entières, deux unités d'exécution d'instructions arithmétiques à virgule flottante, trois unités de chargement -- déchargement de registre, un pipe-line à dix étages.
© copyright Éditions Vuibert et Laurent Bloch 2003
Page d'accueil de ce livre
Précédent Remonter Suivant