JSimRisc : un outil pédagogique pour appréhender les techniques avancées mises en oeuvre dans les processeurs récents


Résumé

Pour aborder convenablement l'enseignement des architectures des processeurs récents, il est souhaitable de proposer aux étudiants des séances de travaux pratiques illustrant simplement des mécanismes complexes. La manipulation directe d'un processeur ne permet pas facilement de comprendre des fonctionnements internes complexes. En effet, un bon nombre de techniques est implanté directement dans les étages pipeline du processeur et il est délicat de suivre convenablement l'état du processeur cycle à cycle. Pour faciliter la compréhension interne du processeur il est alors possible de s'appuyer sur des outils de simulation de processeur. Ces outils basé, sur une l'émulation d'un processeur, permettent de s'affranchir de la cible matérielle. Il existe plusieurs outils disponibles sur Internet qui offrent c type de comportement. Jusqu'à récemment , nous utilisions l'outil DlxView. L'interface graphique de cet outil ainsi que l'architecture simulée issue du livre de Hennessy et Patterson est un bon support pour aider à la compréhension de mécanismes complexes des récents processeurs. Toutefois, il s'avère que certains mécanismes ne sont pas pris en charge par cet outil. On citera par exemple, les mécanismes de prédiction de branchement, de branchement retardé, de disponibilité d'instruction conditionnelle. En connaissance des faiblesses de DlxView, nous avons entamé le développement, en interne, d'un outil de simulation de processeur Risc permettant, entre autres, d'illustrer les techniques précédemment citées. Cet outil, JSimRisc, a été utilisé pour la première fois en travaux pratiques durant l'année universitaire 2003-2004. L'outil nous a permis de couvrir les techniques qui étaient précédemment illustrées par DlxView, mais nous a également permis de couvrir un plus large spectre de techniques.

Dans ce document, nous présentons les objectifs de cet outil qui sont étroitement liés aux objectifs pédagogiques de l'enseignement des architectures des processeurs, puis nous présentons la structure logicielle que nous avons mise en place, enfin nous illustrons l'utilisation de cet outil au travers de quelques exemples montrant à la fois le côté convivial de l'interface graphique développée et le côté pédagogique du simulateur. Les exemples sont présentés sous l'angle de la problématique posée par le pipeline et des solutions apportées par les constructeurs de processeurs.

Introduction

L'enseignement des architectures des processeurs est un aspect important de la formation des ingénieurs de la filière Electronique et Informatique Industrielle de l'Enssat (Ecole Nationale Supérieure des Sciences Appliquées et de Technologie). Nous pensons que ces futurs ingénieurs seront, au cours de leur carrière, confrontés aux développements de codes sur des processeurs de type traitement du signal (DSP). Or, il est évident que dans ce contexte, les algorithmes à implémenter sont toujours assujetis à une contrainte de temps d'exécution qui conduit globalement à ce que les développeurs soient amenés à écrire ou à retoucher le code assembleur à hauteur de 80 % du code total de l'application. Partant de cette constatation, il est alors important que nos futurs ingénieurs aient des connaissances assez précises concernant les techniques qui sont mises en oeuvre dans les processeurs récents, techniques qui migrent petit à petit vers les processeurs de traitement du signal.

Les étudiants abordent le thème des architectures de processeurs par un premier module qui les immerge en douceur dans le domaine de l'architecture des processeurs. Ce premier contact est réalisé grace à la présentation du modèle de processeur Von Neumann. L'évolution des processeurs est soulignée en montrant, notamment, l'évolution importante de la complexité des jeux d'instructions. La famille des processeurs Intel sert de base à l'illustration de cette évolution pour des raisons évidentes de notoriété de ce constructeur. Le concept de machine Risc est ensuite présenté en montrant que l'évolution des jeux d'instructions a conduit à une utilisation très disparate des instructions d'un processeur complexe et en indiquant que certaines instructions sont finalement très peu utilisées par les programmes classiques. La règle des 90-10 est énoncée, indiquant que le processeur passe 90 % de son temps à exécuter seulement 10 % de son jeu d'instructions.

Vient ensuite la présentation du concept de fonctionnement à la chaîne, ou autrement dit de fonctionnement pipeline. Le principe de fonctionnement étant posé, on montre tout l'intérêt d'un tel fonctionnement et on insiste sur le fait qu'un fonctionnement de ce type n'est vraiment intéressant que dans le cas où le pipeline est alimenté de façon permanente. On montre ensuite comment les processeurs implémentent ce concept en découpant le contrôleur d'exécution des instructions en étapes de base (classiquement, les étapes de Lecture d'Instruction, de Décodage d'Instruction, d'Exécution d'Instruction et de Rangement de Résultat).

La suite de cet enseignement traite ensuite des problèmes qui sont engendrés par ce fonctionnement à la chaîne. On décortique le fonctionnement du processeur, et à chaque étape, on identifie le problème qui se pose, puis on montre comment les constructeurs contournent le problème. Les problèmes de dépendances de données sont pointés, de même que les problèmes liés à l'exécution des branchements, et plus particulièrement des branchements conditionnels. Les solutions proposées (les techniques de renommage de registres, de mise en place de buffers d'instructions, d'exécution dans le désordre, de prédictions de branchement, etc) sont alors présentées et illustrées autant que faire se peut dans le cadre du cours et des séances de travaux dirigés.

Pour compléter ces illustrations, il nous est apparut important de confronter les étudiants à ces techniques. L'utilisation directe d'un processeur étant quelque peu délicate en raison de la difficulté à accéder à l'ensemble des étages pipelines à chaque cycle, cela nous a conduit à nous tourner vers un simulateur de processeur. Notre choix initial de simulateur a été guidé par le livre de Hennessy et Patterson. En effet, les auteurs de ce livre appuient leur discours sur une architecture virtuelle : l'architecture DLX et l'existance d'un simulateur de processeurs répondant à cette architecture à évidemment été un critère important pour notre choix. Nous avons donc choisi le simulateur DlxView.

Bien que très convivial et pédagogiquement bien conçu, le simulateur DlxView n'offre pas une couverture suffisante pour aborder des techniques telles que la prédiction de branchement par exemple. De même, ce simulateur ne permet pas d'évaluer le fonctionnement d'autres modèles architecturaux tels que le modèle VLIW (Very Long Instruction Word). Pour tenter de couvrir un plus large spectre des techniques avancées mises en oeuvre dans les processeurs, nous avons entamé le développement, en interne, d'outils. Dans le cadre de ce document, nous présentons uniquement l'outil JSimRisc qui étend DlxView à la prise en compte des techniques de prédictions de branchement. On trouvera sur le site http://r2d2.enssat.fr des informations relatives aux autres développements effectués ou en cours (simulateur de processeur VonNeumann : JSimVEM, simulateur de processeur VLIW : VPS, simulateur de mémoire cache JSimCache).

La suite de ce document présente l'infrastructure logicielle mise en place ainsi que l'organisation globale du système développé. Nous présentons ensuite une série d'exemples en partant à chaque fois de problèmes spécifiques liés au fonctionnement pipeline.

Infrastructure logicielle développée

Lors de l'étude de conception de ce simulateur, nous avons souhaité avoir une organisation facilitant le développement et la maintenance de l'outil. De plus, nous souhaitions pouvoir réutiliser des composants développés dans ce simulateur, pour d'autres outils de la même famille. Nous avons aussi souhaité que ce simulateur de processeurs puisse être manipulé de deux manières différentes : par l'intermédiaire d'un terminal, ou par l'intermédiaire d'une interface graphique conviviale.

Cet ensemble d'objectifs nous a conduit à organiser notre outil en quatre niveaux (voir figure ci-dessous) :

Limitations actuelles du simulateur

Les travaux de développement que nous avons entamés nous ont conduis à limiter, dans un premier temps, le nombre de techniques à couvrir. Nous souhaitions offrir au moins les mêmes fonctionnalités que DlxView tout en ayant la possibilité de faire évoluer le modèle du processeur.

Les techniques retenues dans la premiere version de notre simulateur sont :

Actuellement, le modèle de processeur ne permet pas d'illustrer certaines techniques. Ces limitations sont les suivantes :

Il est probable que ces limitations seront levées dans les versions futures de l'outil.

Exemples

Avant de présenter les exemples, nous proposons un petit rappel de ce que l'on entend par fonctionnement pipeline d'un contrôleur de processeur. Il faut être conscient que l'introduction de ce mode de fonctionnement a été une avancée très importante pour les performances des processeurs. En effet, ce traitement à la chaîne des instructions permet d'augmenter la cadence à laquelle le processeur est en mesure de consommer le programme à exécuter.

Rappel sur le fonctionnement pipeline

Tous les processeurs récents (hormis peut être les processeurs enfouis) intègrent la notion de fonctionnement pipeline. L'intérêt principal du pipeline (ou traitement à la chaîne) réside dans la possibilité de traiter plusieurs instructions en même temps dans le processeur. Dans ce type de fonctionnement, les différentes instructions qui sont traitées par le processeur ne se trouvent pas à la même étape, elles sont chacune à un niveau de traitement différent.

On donne sur le schéma ci-dessous une représentation très générale du fonctionnement d'un pipeline. Une série d'instructions est représentée sur la gauche et l'on voit comment ces instructions vont traverser les différentes étapes.

Pour bien comprendre le mode de fonctionnement, il faut observer ce qui se passe cycle par cycle :

On arrête là la description du fonctionnement du pipeline du processeur. La suite du fonctionnement est identique. Notons tout de même que l'hypothèse qui est faite dans la représentation ci-dessus repose sur une indépendance complète des instructions les unes par rapport aux autres. Ce qui n'est évidemment pas toujours le cas.

Par rapport à un processeur Von Neumann classique (c'est-à-dire non pipeline), il est évident que la mise en place d'un traitement à la chaîne des instructions permet d'augmenter la fréquence d'horloge. En effet, ce traitement à la chaîne permet de scinder le traitement d'une instruction en plusieurs petites étapes et même si chaque instruction n'est pas traitée plus rapidement, le processeur consomme le programme beaucoup plus vite. Pour s'en convaincre, observons la figure ci-dessous.

Comparons la vitesse à laquelle les deux contrôleurs ci-dessus consomment les instructions :

Si l'on considère une suite infinie d'instructions entrant dans le pipeline, alors on constate que le processeur dont le contrôleur est pipeliné consomme le programme quatre fois plus vite que le processeur Von Neumann.

Les tendances actuelles dans les processeurs récents montrent que les pipelines s'allongent de façon très importante. Le processeur Pentium IV, par exemple, est découpé en 20 étages. Cette découpe du traitement d'une instruction en un grand nombre d'étages permet, notamment, au constructeur de pouvoir augmenter la fréquence d'horloge du processeur, ce qui est un argument commercial important.

Toutefois, il ne faut pas se voiler la face, cet allongement du pipeline d'exécution génère quelques difficultés quant à une utilisation efficace de celui-ci. Le code de haut niveau ci-dessous montre une dépendance de données (dépendance sur la donnée a qui va donner naissance à une dépendance de données au niveau assembleur.

      a = b + c ;
      d = a + e ;

Le code assmbleur généré par le compilateur sera alros le suivant


      ADD R1, R2, R3    // R1 = R2 + R3
      ADD R4, R1, R5    // R4 = R1 + R5

Il est clair que pour un processeur non pipeline, l'exécution de ces deux instructions ne pose aucun problème particulier. Par contre, dès lors que le processeur dispose d'un contrôleur pipeliné, alors peuvent survenir des problèmes qui engendrent une exécution incorrecte du programme.

Nous détaillons, dans les sections suivantes, les problèmes engendrés par le pipeline et nous expliquons les techniques proposées par les constructeurs.

Problèmes de dépendances de données

Les applications classiques manipulent des données qui sont plus ou moins liées les unes aux autres. Cela se traduit par un code assembleur dont les instructions dépendent les unes des autres. Ces dépendances sont le fait des registres manipulés par les instructions. Prenons l'exemple du code ci-dessous :

      ADD R1, R2, R3    // R1 = R2 + R3
      ADD R4, R1, R5    // R4 = R1 + R5

Si aucun mécanisme particulier n'est mis en place pour détecter la dépendance créée par le registre R1 produit par la première instruction, puis lu par la seconde instruction, alors le fonctionnement du programme peut être incorrect.

Sur la figure ci-dessus, on montre le déroulement de ces deux instructions dans le cas d'un processeur pipeliné sur 7 étages. La figure montre à quels instants les instructions vont manipuler l'opérande R1. On pose comme hypothèse que les opérandes sources sont lues au cycle E1 et que les opérandes destinations sont écrites au cycle E4. En imaginant que les valeurs des registres soient les suivantes :

L'exécution dans le pipeline va donner les résultats suivants :

La valeur contenue au cycle 3 dans le registre R1 ne peut pas être le résultat de l'instruction précédente, puisque celle-ci n'a pas encore écrit dans son opérande destination. On voit donc clairement, sur cet exemple simple, que le pipelinage du processeur provoque une exécution incorrecte du programme. Ce programme est pourtant bien correct, et n'importe quel développeur s'attend, en regardant ce bout de code, à ce que la seconde instruction effectue son calcul avec la valeur de R1 produite par la première instruction. Le résultat attendu dans le regitsre R4 est la valeur 27 ((12 + 10) + 5 = 27) et non la valeur 7 ((2 + 5) + 5 = 27)

Les techniques développées pour résoudre ce problème sont présentées ci-dessous. La première technique consiste simplement à stopper l'avancement des instructions dans le pipeline jusqu'à ce que toutes les opérandes sources de la seconde instruction soient disponibles. D'autres techniques proposent de casser cette dépendance de données soit par utilisation d'autres registres du processeur, soit par remise en cause de l'ordre d'exécution des instructions.

Blocage du pipeline

La solution la plus simple qui vient à l'esprit consiste à bloquer l'avancement de la seconde instruction pendant quelques cycles, puis à la débloquer afin que sa phase de lecture d'opérandes sources lui fournisse la bonne valeur pour R1. Dans ce cas, le pipeline devra être bloqué durant 3 cycles comme le montre la figure ci-dessous.

Pour parvenir à un fonctionnement correct, le processeur doit posséder le moyen matériel de déterminer la dépendance entre les instructions et il doit prendre, dynamiquement, la décision de bloquer la seconde instruction dans l'étage DI. Ce bloquage provoque évidemment le blocage de tous les étages précédents du pipeline, mais laisse les étages suivants effectuer leur travail. Au cycle 5, la première instruction termine son exécution par l'écriture dans le registre résultat R1. Cette écriture dans R1 va provoquer le débloquage de la seconde instruction. Le résultat de l'exécution de ces 2 instructions par le processeur pipeline est alors correct.

Solution 2 : Renommage de registres

Solution 4 : Exécution dans le désordre

Problèmes des instructions de branchements conditionnels

Le fonctionnement pipeline pose des problèmes de dépendance particuliers lorsque le processeur doit exécuter des instructions de branchement conditionnel. En effet, les instructions de branchement conditionnel ont la particularité d'avoir un comportement qui dépend d'une condition qui, dans certains cas, peut ne pas être disponible à l'instant souhaité. Prenons l'exemple du branchement conditionnel suivant :

      @i          SUB R1, R1, 1    // R1 = R1 - 1
      @i+1        BRZ R1, @label   // si R1 == 0 alors on saute a l'adresse label

Pour un processeur Von Neumann classique (non pipeline), l'exécution de l'instruction SUB va positionner une valeur dans le registre R1. Cette valeur sera ensuite lue par l'instruction de branchement conditionnel BRZ et utilisée pour décider si le saut doit être pris ou pas. L'instruction suivante qui sera lue par le processeur sera donc soit l'instruction de l'adresse @i+2 si la condition est fausse, soit l'instruction de l'adresse @label si la condition est vraie.

Dans le cas d'un processeur pipeliné, la difficulté tient à l'indisponibilité de la condition au moment où l'on doit décider de charger la prochaine instruction. Illustrons cela par la figure ci-dessous.

La première instruction va modifier le contenu du registre R1 au cycle 5. Ce qui permettra alors à la seconde instruction de savoir, au cycle 6, si le saut doit être exécuté ou non. Mais regardons ce qui s'est passé dans les cycles précédents et imaginons que le branchement attende la fin de l'exécution du SUB pour décider de son comportement.

Arrêtons là l'analyse et revenons sur le cycle 2. Ce cycle fait apparaître un problème. En effet, ne connaissant pas la valeur du registre R1 le branchement ne va pourvoir déterminer comment il doit se comporter.

Une solution simple pour résoudre le problème est d'appliquer la technique du blocage du pipeline jusqu'à ce que l'opérande source du branchement soit disponible. Cela nécessite donc de bloquer le pipeline durant cinq cycles, avant de pouvoir débloquer l'instruction de branchement. Cette solution est difficilement envisageable compte tenu du nombre d'instructions de branchement conditionnel présent dans les programmes classiques (environ une instruction sur cinq).

L'une des solutions possibles consiste à laisser le pipeline lire les instructions qui suivent le branchement et à les annuler s'il le faut. Dans le cas ci-dessus, on constate qu'entre le moment où le branchement entre dans le pipeline et le moment où l'on est capable de savoir comment exécuter ce branchement, 5 instructions ont été lues par le processeur. Au cycle 5, le registre R1 est produit, ce qui permet, au cycle 6 de connaître le comportement du branchement. A partir de là, deux cas de figures peuvent se présenter :

La solution technique mise en oeuvre pour annuler les instructions est une invalidation des écritures des opérandes destinations. Le fait de rendre l'écriture dans les registres résultats inactifs, transforme toutes ces instructions en instructions de type Nop. Cette solution, bien que donnant un fonctionnement correct, n'est pas satisfaisante car elle coûte cinq cycles processeur à chaque fois qu'un branchement conditionnel est pris. Ce problème est d'autant plus critique que le nombre de branchements dans les applications est globalement égal à 20 % (c'est-à-dire qu'une instruction sur cinq est un branchement). Dans ces conditions, il devient difficilement acceptable de perdre autant de cycles lors de l'exécution de ces applications.

La solution technique pour éviter de perdre trop de cycles processeur lorsqu'un branchement conditionnel est exécuté par le processeur, consiste à tenter de prédire comment va se comporter le branchement avant même que la condition ne soit connue (calculée).

Lorsque l'instruction de branchement est lue par l'étage de lecture au cycle 1, le processeur va tenter de prédire son comportement. Le processeur va donc calculer une hypothèse concernant la suite des instructions à exécuter. Il va donc décider de :

Le cycle suivant, le processeur va donc faire entrer dans son pipeline des instructions dont il suppose l'exécution. On qualifie ces instructions de prédites, puisque l'on prédit leur exécution sans pour autant être sur qu'elles sont à exécuter. Lorsque l'instruction Sub termine son exécution, alors le processeur peut vérifier l'hypothèse qu'il avait faite. Deux situations peuvent alors se produire :

Voilà simplement la technique de la prédiction de branchement à mettre en oeuvre et finalement, la problématique des branchements conditionnels est alors ramenée à un problème de calcul d'hypothèse sur le comportement de ces branchements. Si l'on est capable de prévoir le comportement de tous les branchements conditionnels sans jamais se tromper, alors aucun cycle processeur ne sera perdu à cause des branchements. Par contre, il est clair que si, pour chaque branchement conditionnel, l'hypothèse calculée se révèle fausse, alors le nombre de cycles perdus sera très important.

La question qui se pose alors est la suivante :

Comment prévoir le comportement d'un branchement ?

Plusieurs prédicteurs ont été développés. Les plus anciens prédicteurs étaient simples et peu coûteux à implémenter, mais ils ont rapidement été remplacés par des prédicteurs plus complexes offrant de bien meilleures performances (on appelle performance d'un prédicteur de branchement le taux de bonnes prédictions de celui-ci). Il est relativement aisé d'observer que le nombre de cycles perdus en cas de mauvaise prédiction dépend de la longueur du pipeline. Cette observation n'empêche pas pour autant les constructeurs à allonger le pipeline de leurs processeurs. Il leur faut donc compenser l'augmentation du nombre de cycles perdus lors d'une mauvaise prédiction par un taux de bonnes prédictions plus élevé. Des pédicteurs avec des taux de bonnes prédictions voisins de 99%, et dans certains cas même supérieur, ont alors été développés.

Les sections suivantes vont détailler les différentes techniques que l'on peut globalement classer en deux catégories qui sont les techniques statiques et dynamiques.

Prédictions de branchement statiques

Ces techniques sont dites statiques parce qu'elles proposent une méthode globale à tous les branchements pour calculer l'hypothèse.

Prédiction de branchement statique globale

La première technique consiste à fixer une méthode (pour le calcul de l'hypothèse) unique pour tous les branchements conditionnels de l'application. Par exemple, à chaque fois que le processeur se trouve face à une instruction de branchement conditionnel, celui-ci pose comme hypothèse de prendre le branchement. Cette technique est évidemment peu souple et offre des performances (taux de bonnes prédictions) assez faibles. Elle a toutefois l'avantage d'être peu coûteuse.

Prédiction de branchement statique positionnable

Conscient des piètres performances de la première technique et sachant que le comportement moyen des branchements peut être différent d'une application à l'autre, certains constructeurs ont proposé un mécanisme permettant de positionner le sens de la prédiction. Par exemple, pour l'application 1, le processeur peut avoir une hypothèse telle que les branchements sont prédits pris, alors que pour une application 2, le processeur peut avoir l'hypothèse inverse (c'est-à-dire que tout branchement est prédit non pris). Le positionnement du sens de la prédiction est à la discrétion du développeur ou du compilateur. On peut dans ce cas s'appuyer sur une étude de profiling de l'application afin de positionner, de la manière la plus adéquate possible, le sens de la prédiction.

Prédiction de branchement statique tenant compte du sens du branchement

Des études sur le comportement des branchements ont montré que le comportement moyen de ceux-ci dépendait beaucoup de leur sens. La notion de sens des branchements permet de distinguer les branchements arrières et les branchements avants :

Il a été montré que les branchements arrières ont plutôt tendance à être pris par le processeur. En effet, les branchements conditionneles arrières résultent très souvent de la compilation de boucles. Or la caractéristique principale d'une boucle est évidemment que les instructions situées dans la boucle vont être exécuter plusieurs fois. On donne ci-dessous le code de haut niveau d'une boucle exécutant N fois un calcul élémentaire.

     for (i = 0 ; i < N ; i++) {
         x[i] = u[i] + v[i];
     }

La compilation va générer un code assembleur du type de celui donné ci-dessous.

     Program   LI   R0, @U
               LI   R1, @V
               LI   R2, @X
               LI   R3, N
     Boucle
               LI   R4, (R0)
               LI   R5, (R1)
               ADD  R6, R5, R4
               LI   (R2), R6
               ADD  R0, R0, 1
               ADD  R1, R1, 1
               ADD  R2, R2, 1
               SUB  R3, R3, 1
               BRNZ R3, Boucle
     End       EXIT

Le comportement du branchement conditionnel arrière est presque toujours le même. En effet, ce branchement sera pris N fois, puis il sera non pris 1 fois.

Pour ce qui concerne les branchements avants, ils sont typiquement issus de la compilation d'un code ayant une structure si, alors, sinon telle que présentée dans le code ci-dessous. Or, il a été montré, même si c'est moins évident, qu'ils ont plutôt tendance à être non pris.

     if (condition) else {
        instructions ;
     } else {
       instructions ;
     }

Compte tenu des résultats énoncés ci-dessus, il apparaît alors évident que si le processeur dispose de moyen pour détecter le sens du branchement, alors il pourra disposer de politiques de prédiction de branchement différentes, donc d'avoir un taux de bonnes prédictions meilleur que dans le cas d'une prédiction de branchement statique de base.

Prédictions de branchement dynamiques

Les techniques de prédiction de branchement statique donnent des performances assez faibles et en tout cas pas suffisante dans le cas de pipelines très longs. Cela est notamment du au fait que ces techniques sont globales à tout ou partie des branchements d'une application. Elles ne tiennent donc pas du compte du comportement spécifique de chaque branchement. L'idée qui a alors été proposée consiste à tenter de conserver un historique du comportement de chaque branchement afin de mieux prévoir le comportement des prochaines exécutions de ces branchements. En effet, pour un branchement dont tous les comportements précédents ont conduit le processeur a effectuer un saut, il semble intéressant de conserver cette information et de l'exploiter lorsque ce branchement sera à nouveau a exécuter. Si le branchement a été, historiquement pris(respectivement non pris), alors il sera prédit comme devant être pris (respectivement non pris) une nouvelle fois.

La question qui se pose alors est de savoir comment on conserve cet historique. La première solution développé a consisté à attribuer un bit d'historique par branchement. Une seconde solution a ensuite été développé accordant 2 bits pour le stockage du comportement de chaque branchement.

Une table pour le stockage de cet historique des branchements est alors mise en place dans le processeur. Comme sa taille est forcement limitée et que tous les historiques de tous les branchements ne peuvent pas être conservés, alors le fonctionnement de cette table d'historique est assimilable à un cache. Ne sont conservés que les historiques des branchements les plus récemment exécutés. Cette table est ensuite utitlisée par le processeur afin de calculer l'hypothèse (la prédiction) et elle est mise à jour lorsque le branchement s'exécute (vérification hypothèse). La figure ci-dessous montre dans quels étages cette table est utilisée pour permettre une cohérence dans le processeur.

Prédiction de branchement dynamique à 1 bit

La prédiction de branchement dynamique à 1 bit met en oeuvre un seul bit pour stocker le comportement de chaque branchement. Ce bit mémorise alors un historique peu ancien indiquant simplement comment le branchement s'est comporté lors de l'exécution précédente.

Cette technique permet d'exploiter le caractère spécifique du comportement de chaque branchement conditionnel, mais l'historique conservé étant faible, ce prédicteur n'est pas très performant.

Prédiction de branchement dynamique à 2 bits

Les performances plutôt décevantes du prédicteur dynamique à 1 bit, ont amené les concepteurs a proposer un prédicteur conservant un historique plus important. Une solution mettant en oeuvre 2 bits pour stocker l'historique des branchements a alors été proposée. Les 2 bits de stockage de l'historique sont à nouveau utilisés pour calculer l'hypothèse. On représente ces valeurs possibles de ces 2 bits par une machine d'états dont les états sont les suivantes (voire aussi illustration dans la figure suivante) :

Les états vont permettre le calcul de la prédiction de branchement. Si la machine se trouve dans l'état P ou FP alors le branchement est prédit pris. Inversement, si la machine se trouve dans l'état NP ou FNP alors le branchement est prédit non pris.

Lors de l'exécution effective du branchement, le processeur vient faire une mise à jour de la machine d'états. Les mises à jour sont symbolisées par les changements d'états (flèches d'état à état sur la figure si-dessus).

Suppression des instructions de branchement conditionnel (conditionnel move)

Nous avons donné dans les sections précédentes, les solutions qui peuvent être mises en oeuvre dans le cas où le processeur doit exécuter des instructions de branchements conditionnels. Les constructeurs ont déployé des efforts importants pour fournir des prédicteurs de branchements efficaces. Il n'en reste pas moins que le problème est de plus en plus critique lorsque le pipeline du processeur s'allonge.

Il existe une autre voie pour limiter les risques de pertes de cycles dans les contrôleurs pipelines des processeurs dans le cas d'un branchement. Il s'agit tout simplement d'essayer de supprimer les instructions de branchements conditonnels. Cette solution qui peut sembler, a priori, utopique est toutefois réalisable par le biais du conditionnement de toutes les instructions du processeur. Considérons le code suivant :

     if (condition) then {
        instruction1 ;
        instruction2 ;
     } else {
        instruction3 ;
        instruction4 ;
     }

La compilation de ce code va donner un listing assembleur du type de celui ci-dessous

               ...
               BR  condition, @then
     else
               instruction3 ;
               instruction4 ;
               BRA @suite
     then
               instruction1 ;
               instruction2 ;
     suite
               ...
               ...

Ce code fait nécessairement apparaître un branchement conditionnel dont le calcul d'hypothèse est assez délicat. L'idée consiste alors à proposer de supprimer ce branchement conditionnel au profit d'instructions dont l'exécution est rendue conditionnelle. Nous donnons ci-dessous un exemple de code assembleur avec utilisation d'instructions conditionnelles.

             p1, p2     = eval (condition)
     <p1>                instruction1 ;
     <p1>                instruction2 ;
     <p2>                instruction3 ;
     <p2>                instruction4 ;

La première instruction de ce code permet une évaluation de la condition. Cette évaluation est utilisée pour positionner deux registres booléen spécifiques. Le premier registre est positionné à true si la condition est vraie, ou à false si la condition est fausse. Le second registre prend la valeur complémentaire.

Les instructions suivantes (partie the et else) sont ensuite alignées les unes à la suite des autres sans faire de saut. Les instructions circuleront donc dans le pipeline du processeur, mais seules deux de ces instructions auront s'exécuteront normalement, les deux autres auront le comportement d'instruction Nop. Les instructions qui s'exécuteront normalement sont celles dont le registre pi est vraie.

illustrations au travers de JSimRisc

Installation de JSimRisc

  1. Version Windows :
  2. Version Unix & Linux :

Découverte de l'interface

Une fois l'outil lancé, vous devez voir apparaitre la fenêtre suivante :
Cette fenêtre se découpe en 5 zones qui sont :

Premiers pas avec JSimRisc

Conclusion

Ce document présente un outil dont l'objectif est l'illustration des mécanismes complexes qui sont mis en oeuvre dans les processeurs pipelines. Cet outil palie aux manques de DlxView par la prise en compte de certaines techniques notamment celles-liées aux mécanismes de prédiction de branchement. Cet outil a été développé avec un objectif pédagogique avoué. L'interface graphique propose une vue complète mais toutefois simple du processeur. Les éléments principaux de l'architecture du processeur sont présentés et leur évolution est assurée au fur et çà mesure de l'exécution des cycles processeur.

Cet outil complète, au travers de manipulations, la présentation de ces mécanismes sous forme magistrale et d'exercices. Nous proposons aux étudiants une application écrite naivement et nous leur proposons d'analyser le déroulement de cette application sur le processeur. L'analyse, en mode pas à pas, leur permet de comprendre où sont les problèmes. Nous leur proposons alors ensuite d'optimiser le code de l'application afin de répondre à une contrainte en nombre de cycles. Le gain que l'on attend en terme d'optimisation est supérieur à 50% (le nombre de cycles doit être plus que divisé par 2). Le déroulement des travaux pratiques, organisés sous une forme concours à l'optimisation maximale, montre que les étudiants confortent leur compréhension du fonctionnement d'un processeur pipeline.


Informations techniques concernant JSimRisc

JSimRisc a été développé avec pour principal objectif l'illustration du cours d'architecture des processeurs, dans lequel les mécanismes récents qui permettent d'augmenter les performances des processeurs sont presentés.

Quelques détails techniques :

Liste des instructions supportées :

1) Instructions de transferts
li, si chargement/rangement mot vers/depuis des registres entiers
lf, sf chargement/rangement mot vers/depuis des registres flottants
move transfert entre registres
cmovz, cmovnz, cmovp, cmovp transfert conditionnel entre registres
2) Instructions arithmétiques et logiques
add, addf addition sur des entiers et flottants
sub, subf soustraction sur des entiers et flottants
mult, multf multiplication sur des entiers et flottants
and, nand et, non et
or, nor, xor ou, non ou, ou exclusif
3) Instructions de contrôle
br branchement inconditionnel
brz, brnz branchement si registre égal/non égal à zéro
brp, brn branchement si registre positif/négatif
nop opération qui ne fait rien
exit fin du programme