Fonctionnement d'un ordinateur/Les premiers processeurs Intel
Les chapitres précédents ont été assez chargés, et nous ont appris comment fonctionne un pipeline simple. Découper un processeur en étape, gérer les branchements, découpler les étages avec des FIFOs, gérer les dépendances de données, implémenter le contournement. De nombreux processeurs RISC intègrent un pipeline similaire à celui décrit dans les chapitres précédents, souvent implémenté afin d'avoir de bonnes performances. L'implémentation est claire, classique, assez propre. En comparaison, les premiers processeurs Intel avaient des pipeline bien différents, suffisamment pour les aborder dans un chapitre dédié.
Par premiers CPU Intel, nous voulons parler des processeurs avant le Pentium. Il s'agit du 8086, du 286, du 386 et du 486. Le Pentium est un peu particulier, car c'est un processeur superscalaire, ce qui fait qu'on n'en parlera pas ici. Les premiers CPU Intel avaient un pipeline très simple, assez semblable à un pipeline Fetch-Decode-Exec. Cependant, l'implémentation de ce pipeline a été compliquée par plusieurs point de blocage. Le premier est le fait que ce soit des processeurs CISC, avec beaucoup d'instructions microcodées. Le second est l'absence de mémoire cache intégrée (sauf pour le 486) et le fait que ce ne soient pas des architectures Harvard. Il est intéressant de voir les premiers CPU Intel pour voir comment les processeurs CISC se sont débrouillés pour surmonter ces problèmes.
Les pipelines faiblement et fortement couplés
[modifier | modifier le wikicode]L'évolution des processeurs Intel a été une lente transition d'un type de pipeline vers un autre. Le 8086, le 286 et le 386 utilisaient des pipelines faiblement couplés, alors que le 486 utilisait un pipeline fortement couplé. Les pipelines fortement couplés équilibrent leurs étages au mieux, afin que chaque étage prennent approximativement le même temps. Le résultat est que chaque étage fait son travail en un cycle d'horloge, pas plus. À l'opposé, les pipelines faiblement couplés ont des étages inégaux, dont certains font plusieurs cycles d'horloge.
Précisons que la définition n'interdit pas d'avoir des instructions multicycles. Les accès mémoire et quelques instructions complexes peuvent passer plusieurs cycles dans le même étage. Mais pour les instructions simples, à savoir les additions/soustractions/décalages/opérations bit à bit ; qui ne font pas d'accès mémoire et agissent uniquement sur des registres. Elles doivent idéalement s'exécuter en un seul cycle sur un pipeline fortement couplé. De même, il est possible d'avoir des instructions simples qui prennent plusieurs cycles, à condition qu'elles soient pipelinées. Par exemple, si une addition prend deux cycles, alors elle s'exécute en deux étapes qui peuvent exécuter deux calculs différents en même temps : un dans le premier étage, un autre dans le second étage.
Pour donner un exemple, sur le 286 et le 386, beaucoup d'opérations basiques prenaient plusieurs cycles. Les additions, soustractions, opérations bit à bit et décalages prenaient toutes 2 cycles d'horloge. Et elles n'étaient pas pipelinées, ce qui fait que l'unité de calcul bloquait le pipeline quand une instruction s'exécutait dedans. Et il en est de même avec l'étage de décodage : il pouvait décoder une instructions simples en un seul cycle d'horloge, mais les instructions complexes prenaient plus de temps à décoder.
Les pipelines fortement couplés peuvent exécuter plusieurs instructions en même temps sans trop de problèmes, mais il est fréquent que certains étages soient inutilisés, même dans des circonstances optimales. Au contraire, un pipeline fortement couplé s'approche plus facilement de son optimum théorique. Un pipeline fortement couplé à N étages s'approchera des N instructions exécutées en même temps, alors qu'un pipeline faiblement couplé aura tendance à avoir des étages inutilisés.
Les pipelines à registres et à FIFO
[modifier | modifier le wikicode]Dans les implémentations les plus simples, les pipelines fortement couplés utilisent des registres entre chaque étage, alors que les faiblement couplés n'ont pas de choix : ils doivent utiliser des FIFOs entre chaque étage. L'implémentation est alors plus simple sur les pipelines fortement couplés, mais cela a un prix : au moindre problème (dépendance de données, branchements), le pipeline stalle pour éviter tout problème. Un pipeline faiblement couplé n'a pas ce problème, certains étages peuvent prendre de l'avance au lieu de se bloquer.
Et cette flexibilité en cas de stall explique que le 286 et le 386 utilisaient des pipelines faiblement couplés, comme nous allons le voir. Sans compter que l'implémentation était aussi plus simple. Avoir des étages déséquilibrés permettait une implémentation simple. Il suffisait de placer une file d'instructions et une file de micro-opérations pour obtenir un pipeline fetch-decode-exec. Pas besoin de découper le décodeur pour le pipeliner, pas besoin de pipeliner l'ALU, on les laisse intacts.
Mais il s'agit là des implémentations les plus simples. De nos jours, de nombreux pipelines fortement couplés utilisent des FIFOs pour découpler leurs étages, ce qui permet d'avoir le meilleur des deux mondes. L'usage de files d'instruction et de files de micro-opérations est courante sur les processeurs haute performance modernes. Mais ils combinent celles-ci avec des pipelines équilibrés, où chaque étage fait un cycle d'horloge. Les deux détails, équilibrage des étages et découplage avec des FIFOs, entrainent un gain de performance, qui s'additionne quand on les combine.
La prefetch input queue : une file d'instruction
[modifier | modifier le wikicode]Les premiers processeurs Intel intégraient une file d’instruction quelque peu particulière, appelée la Prefetch Input Queue, terme abrévié en PIQ. Elle a été utilisée sur tous les processeurs avant le Pentium, à savoir les processeurs Intel 8 et 16 bits, ainsi que sur le 286 et le 386.
Avec la PIQ, pendant qu'une instruction est en cours de décodage/exécution, le processeur précharge les instructions suivantes, jusqu'à une limite de 4/6/16/32 octets (la limite dépend du processeur). La prefetch input queue permet donc d'implémenter une forme de pipeline Fetch + Decode/Exec. Mais il est aussi possible de dire qu'il s'agit d'une technique de préchargement d'instructions, vu qu'elle permet de précharger plusieurs instructions si les circonstances le permettent. La différence entre les deux est assez subtile et les frontières sont assez floues.
La PIQ permet de précharger l'instruction suivante à condition qu'aucun autre accès mémoire n'ait lieu. Les CPU Intel avant le 486 n'avaient pas de cache, ni d'architecture Harvard. La conséquence est qu'il n'est pas possible de lire une instruction et faire un accès mémoire en même temps. Si l'instruction en cours d'exécution effectue des accès mémoire, ceux-ci sont prioritaires sur tout le reste, le préchargement de l'instruction suivante est mis en pause. Le préchargement n'est possible que si l'instruction en cours d'exécution ne travaille que sur des registres.
La PIQ faisait 6 octets sur le 8086 et le 286, puis est montée à 16 octets sur le 386, pour atteindre 32 octets sur le 486. Le 8088, une version simplifiée du 8086, avait une PIQ de 4 octets seulement. La taille idéale de la FIFO dépend de nombreux paramètres. On pourrait croire que plus elle peut contenir d'instructions, mieux c'est, mais ce n'est pas le cas, pour des raisons que nous allons expliquer dans quelques paragraphes.
Vous remarquerez que j'ai exprimé la capacité de la PIQ en octets et non en instructions. La raison est que la PIQ s'est adaptée au fait que le jeu d'instruction x86 a des instructions de taille variable. Ce n'est donc pas tellement une file d'instruction au sens strict. Le jeu d'instruction x86 a des instructions dont la taille varie entre un octet et 15 octets. Vous noterez que la PIQ est mieux exploitée quand les instructions sont courtes, ce qui permet de précharger plus d'instructions.
L'interaction avec le décodage d'instruction
[modifier | modifier le wikicode]La PIQ dispose d'un port d'écriture sur lequel les instructions préchargées sont envoyées, et un port de lecture où le décodeur lit la prochaine instruction à décoder.
La PIQ du 8088 était la plus simple, niveau implémentation. Sur le 8088, tout faisait un octet : le bus de données, les ports de la PIQ, et j'en passe. La PIQ du 8088 était composée de 4 registres de 8 bits, avec un port d'écriture de 8 bits pour précharger les instructions octet par octet, et un port de lecture de 8 bits pour alimenter le décodeur. En clair, tout faisait 8 bits.
Le 8086 était quant à lui un processeur 16 bits, ce qui changeait un peu les choses. Sa PIQ était adaptée à un bus de données de 16 bits, à savoir qu'elle mémorisait ses 6 octets avec 3 registres de 16 bits. Le préchargement des instructions se faisait deux octets à la fois, un registre à la fois, grâce à un port d'écriture de 16 bits. Un détail important est que le préchargement était aligné sur 16 bits en mémoire. Si l'instruction à charger n'était pas alignée sur 16 bits, elle doit être chargée en plusieurs fois. Par exemple, une instruction de 2 octets non-alignée sera chargée en deux fois : un premier accès qui charge deux octets, dont un est oublié, et un second accès pour l'octet restant, dont l'autre octet aussi est oublié.
Par contre, le décodage se faisait un octet à la fois. Le décodeur lit un octet, puis éventuellement le suivant, et ainsi de suite jusqu'à atteindre la fin de l'instruction. Le port de lecture faisait ainsi un octet sur le 8086, comme sur le 8088. Le port de lecture incorporait pour cela un multiplexeur, qui sélectionnait l'octet adéquat dans un registre 16 bits. L'avantage est que le préchargement est plus efficace, car il préchargeait plus d'octets par cycle. Vu que la PIQ ne peut pas précharger d'instruction à chaque cycle, du fait des accès mémoire, le fait de précharger plus à chaque accès compense.

Le 386 était un processeur 32 bits, ce qui fait que sa PIQ contenait 4 registres de 32 bits et un port d'écriture de 32 bits. Et le port de lecture pour le décodeur d'instruction faisait lui aussi 32 bits. Il était cependant capable de gérer des instructions de 8 et 16 bits. Il pouvait dépiler entre 1 et 4 octets dans la PIQ, suivant la taille de l'instruction voulue. De plus, les instructions préchargées n'étaient pas parfaitement alignées sur 32 bits : une instruction pouvait être à cheval sur deux registres 32 bits. Le processeur incorporait donc des circuits d'alignement, semblables à ceux utilisés pour gérer des instructions de longueur variable avec un registre d'instruction.
Les signaux de commande entre décodeur et PIQ
[modifier | modifier le wikicode]Les instructions varient entre 1 et 15 octets, mais tous ne sont pas utiles au décodeur d'instruction. Le décodeur d'instruction n'a besoin que de deux octets : l'opcode et l'octet Mod/RM qui précise le mode d'adressage et les registres. Les octets de préfixe sont aussi utiles au décodeur, mais la plupart des instructions n'en ont pas. Les constantes immédiates, adresses absolues et décalages, ne passent pas par le décodeur d'instruction. Ils sont lus depuis la PIQ, mais sont copiées dans des registres dédiés, sous la commande du décodeur d'instruction. En clair, le décodeur a besoin de lire deux octets maximum depuis la PIQ, avant de passer à l’instruction suivante. Les autres octets étaient envoyés ailleurs, typiquement dans le chemin de données.
Par exemple, prenons le cas d'une addition entre le registre AX et une constante immédiate. L'opcode était envoyé au décodeur, mais pas la constante immédiate. Elle était lue octet par octet et mémorisée dans un registre temporaire placé en entrée de l'ALU. Idem avec les adresses immédiates, qui étaient envoyées dans un registre d'interfaçage mémoire sans passer par le décodeur d'instruction. Pour cela, la PIQ était connectée au bus interne du processeur. De plus, le décodeur dispose d'une micro-opération pour lire un octet depuis la PIQ et le copier ailleurs dans le chemin de données. Ainsi, l'instruction d'addition entre le registre AX et une constante immédiate était composée de plusieurs micro-opérations : une qui lisait la constante immédiate depuis la PIQ, une seconde qui commande l'ALU et fait le calcul.
Le décodeur devait attendre que qu'un moins un octet soit disponible dans la PIQ, pour la lire. Il lisait alors cet octet et déterminait s'il contenait une instruction complète ou non. Si c'est une instruction complète, il la décodait et l'exécutait, puis passait à l'instruction suivante. Sinon, il lit un second octet depuis la PIQ et continue le décodage. Là encore, le décodeur vérifie s'il a une instruction complète, et lit un troisième octet si besoin, puis rebelote avec un quatrième octet lu, etc.
Un circuit appelé le loader synchronisait le décodeur d'instruction et la PIQ. Il fournissait deux bits : un premier bit pour indiquer que le premier octet d'une instruction était disponible dans la PIQ, un second bit pour le second octet. Le loader recevait aussi des signaux de la part du décodeur d'instruction. Le signal Run Next Instruction entrainait la lecture d'une nouvelle instruction, d'un premier octet, qui était alors dépilé de la PIQ. Le décodeur d'instruction pouvait aussi envoyer un signal Next-to-last (NXT), un cycle avant que l'instruction en cours ne termine. Le loader réagissait en préchargeant l'octet suivant. En clair, ce signal permettait de précharger l'instruction suivante avec un cycle d'avance, si celle-ci n'était pas déjà dans la PIQ.
Le décodeur d'instruction dispose aussi de trois micro-opérations SUSP, FLUSH, and CORR, qui agissent sur la PIQ. La micro-opération SUSP stoppe le préchargement des instructions dans la Prefetch input queue. La micro-opération FLUSH réinitialise la PIQ et le préchargement reprend comme avant. Elle sert dans diverses situations. Par exemple, la PIQ était réinitialisée lors du passage du mode réel au mode protégé, et réciproquement. Quelques instructions sont décodées entre les deux modes, ce qui fait que l'on doit vider la PIQ entre les deux modes. L'instruction CORR sert pour gérer les branchements, et expliquer pourquoi demande une section complète, que voici.
Les problèmes liés aux branchements
[modifier | modifier le wikicode]Les branchements posent des problèmes avec la PIQ : si un branchement est pris, toutes les instructions préchargées après sont invalides. Pour éviter cela, la PIQ est réinitialisée quand le processeur détecte un branchement. Le microcode utilise pour cela la micro-opération qui réinitialise la PIQ. Les interruptions posent le même genre de problèmes. Il faut impérativement vider la PIQ quand une interruption survient, avant de la traiter.
La PIQ précharge les instructions séquentiellement, une après l'autre. Pour savoir où elle en est rendue, elle devrait mémoriser l'adresse de la prochaine instruction à précharger dans un registre de préchargement dédié. Il est censé être séparé du program counter, mais ce n'est pas la solution qui a été utilisée par les ingénieurs d'Intel. À la place, le registre de préchargement remplace le program counter. Après tout, le registre de préchargement n'est qu'un program counter ayant pris une avance de quelques cycles d'horloge, qui a été incrémenté en avance.
Mais cela pose problème, avec les branchements. Par exemple, certains branchements relatifs demandent de connaitre la véritable valeur du program counter, pas celle calculée en avance. Idem avec les instructions d'appel de fonction, qui demandent de sauvegarder l'adresse de retour exacte, donc le vrai program counter. De telles situations demandent de connaitre la valeur réelle du program counter, celle sans préchargement. Pour cela, le décodeur d'instruction dispose d'une instruction pour reconstituer le program counter, à savoir corriger le program coutner et éliminer l'effet du préchargement.
La micro-opération de correction se contente de soustraire le nombre d'octets préchargés au registre de préchargement. Le nombre d'octets préchargés est déduit à partir des deux pointeurs intégré à la FIFO, qui indiquent la position du premier et du dernier octet préchargé. Le bit qui indique si la FIFO est vide était aussi utilisé. Les deux pointeurs sont lus depuis la FIFO, et sont envoyés à un circuit qui détermine le nombre d'octets préchargés. Sur le 8086, ce circuit était implémenté non pas par un circuit combinatoire, mais par une mémoire ROM équivalente. La petite taille de la FIFO faisait que les pointeurs étaient très petits et la ROM l'était aussi.
L'interaction avec le code automodifiant
[modifier | modifier le wikicode]Un autre défaut est que la Prefetch input queue se marie assez mal avec du code auto-modifiant. Un code auto-modifiant est un programme qui se modifie lui-même, en remplaçant certaines instructions par d'autres, en en retirant, en en ajoutant, lors de sa propre exécution. De tels programmes sont rares, mais la technique était utilisée dans quelques cas au tout début de l'informatique sur des ordinateurs rudimentaires. Ceux-ci avaient des modes d'adressages tellement limités que gérer des tableaux de taille variable demandait d'utiliser du code auto-modifiant pour écrire des boucles.
Le problème avec la Prefetch input queue survient quand des instructions sont modifiées immédiatement après avoir été préchargées. Les instructions dans la Prefetch input queue sont l'ancienne version, alors que la mémoire RAM contient les instructions modifiées. Gérer ce genre de cas est quelque peu complexe. Il faut en effet vider la Prefetch input queue si le cas arrive, ce qui demande d'identifier les écritures qui écrasent des instructions préchargées. C'est parfaitement possible, mais demande de transformer la Prefetch input queue en une mémoire hybride, à la fois mémoire associative et mémoire FIFO. Cela ne vaut que très rarement le coup, aussi les ingénieurs ne s’embêtent pas à mettre ce correctif en place, le code automodifiant est alors buggé.
Les processeurs 286 et 386 : le découplage du décodage
[modifier | modifier le wikicode]Les processeurs 286, 386 et 486 avaient des possibilités de pipeline plus élaborées, qui dépassaient l'usage d'une Prefetch Input Queue. Cependant, ce pipeline est fortement contrarié à cause d'un point : il n'y a pas de mémoire cache sur ces processeurs. La conséquence est que le processeur ne peut pas lire une instruction en même temps qu'il accède à des données. Le pipeline fonctionne donc bien si le processeur n'exécute pas d'accès mémoire, ou que ceux-ci sont peu fréquents. Le CPU peut alors précharger des instructions dans la PIQ, et le pipeline fonctionne. Mais si les accès mémoire sont trop nombreux, le pipeline n'est presque pas utilisé, car il n'y a pas assez d'instructions dans la PIQ.
La Decoded Instruction Queue
[modifier | modifier le wikicode]L'Intel 286 et 386 ont séparé l'étage de décodage et d'exécution, en plus de rajouter des étages pour le calcul d'adresse. Ils avaient un pipeline à 3-6 étages : 3 pour les instructions classiques, 6 pour les instructions mémoire. Il avait un pipeline Fetch-Decode-Exec pour les instructions classiques, auxquelles il fallait rajouter 3 cycles supplémentaires pour les accès mémoire. Les 3 cycles supplémentaires s'occupaient de la segmentation, de la pagination, et de l'accès au cache lui-même.
Pour cela, les processeurs 286 et 386 ajoutaient une mémoire FIFO en sortie de l'unité de décodage. Du moins, c'est ainsi que l'on peut présenter les choses pour faire simple. Mais je rappelle que les CPU Intel de l'époque avaient un décodeur hybride, mélangeant un séquenceur câblé et un séquenceur microcodé. Et la FIFO était en quelque sorte en plein milieu du décodeur, avant le microcode, mais après le reste.

Pour rappel, les CPU 286/386/486 avaient un décodeur coupé en deux : un prédécodeur alimentait un séquenceur microcodé. Le prédécodeur peut décoder des instructions simples, et peut envoyer les autres instructions au microcode. Le prédécodeur est appelé la Group Decode ROM, bien que ce ne soit pas une ROM. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode.
Les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Leur opcode et les noms de registre sont simplement extraits par le prédécodeur, il n'y a rien de plus à faire. Le prédécodeur les envoie à la file de micro-opérations, et elles sont immédiatement exécutées dans le chemin de données. Les instructions microcodées passent aussi par cette file, mais elles sont envoyés au microcode en sortant de la file. La file en question n'est donc pas tout à fait une file de micro-opérations, ni une file d'instruction. C'est une sorte d'intermédiaire entre les deux, qui peut mémoriser soit des micro-opérations basiques, soit des instructions partiellement décodées à destination du microcode.

Le résultat est un pipeline très simpliste et très peu efficace. Le pipeline fonctionne avec des instructions qui ne passent pas par le microcode. Mais pour les instructions microcodées, le pipeline stalle, il se bloque. Le décodeur, qui sert d'unité d'émission, gèle le pipeline quand il exécute une instruction multicycle. Et cela vaut pour les multiplications et divisions, mais aussi pour tout accès mémoire, instruction load-op inclues. En pratique, le pipeline n'était presque pas utilisé...
Tout cela montre la difficulté d'intégrer un pipeline sur un processeur microcodé. C'est parfaitement possible en pratique, et nous verrons de nombreux exemples dans la suite. Un chapitre entier sera consacré aux processeurs superscalaires x86, qui ont des pipelines particulièrement complexes. Cependant, le 386 et le 486 avaient peu de transistors et devaient microcoder la plupart de leurs instructions. Cela s'est arrangé avec le temps, notamment avec l'intégration de circuits multiplieur/diviseurs, l'augmentation du nombre de registres, et d'autres optimisations.
L'early start des CPU Intel 386
[modifier | modifier le wikicode]Le 386 avait un système de contournement très rudimentaire, bien différent de tout ce qu'on a vu précédemment. Le contournement était appelé l'early start, et faisait du contournement uniquement pour les calculs d'adresse. L'idée est de fournir en avance les opérandes des calculs d'adresse.
Les calculs d'adresse sont réalisés soit par les instructions mémoire, soit par les instructions load-op. Ces instructions ont besoin d'une adresse, éventuellement d'un indice et/ou d'un décalage. Et l'adresse, l'indice ou le décalage peut être calculé par l'instruction qui précède l'accès mémoire. Dans cette situation bien précise, un système de contournement permet de démarrer l'instruction mémoire/load-op un cycle en avance.
Un problème est que l'implémentation était buguée, ce qui a donné naissance au POPAD bug. Après une instruction POPAD, le registre EAX est modifié. Si l'instruction suivante souhaite utiliser ce registre pour un calcul d'adresse, il est possible qu'elle ne voit pas la valeur d'EAX correcte.
L'Intel 486 : un pipeline fortement couplé
[modifier | modifier le wikicode]Les processeurs précédents avaient des pipelines faiblement couplés, ce qui était un moyen de compenser l'absence de mémoire cache et d'architecture Harvard. De tels processeurs ne pouvaient pas charger une instruction et lire/écrire une donnée en même temps. Un pipeline faiblement couplé était assez flexible pour fonctionner dans un tel environnement. Le résultat était loin du maximum théorique qu'on aurait pu obtenir sans ces contraintes, mais le gain en performance était là. Le 486 est passé à un pipeline fortement couplé, avec des étages séparés par des registres.
Le passage à un pipeline fortement couplé
[modifier | modifier le wikicode]Le passage à un pipeline fortement couplé a été rendu possible, ou du moins accompagné, par de nombreux changement dans l'architecture du 486.
La première optimisation est que les opérations basiques se font en un seul cycle d'horloge, pas deux. Les opérations simples en question sont les additions/soustractions, les opérations bit à bit et les décalages. Du moins, à condition qu'elles manipulent uniquement des registres ou des constantes immédiates. Les opérations load-op ne sont pas concernées, elles stallent le pipeline. Pour le dire autrement : l'étage d'exécution passe à un seul cycle d'horloge, ce qui permet une meilleure utilisation du pipeline et réduit le nombre de stalls.
Une seconde optimisation est l'intégration d'une mémoire cache dans le processeur. Il n'y avait pas encore de cache séparé pour les instructions et les données. Mais le cache était multiport, ce qui permettait de lire/écrire une donnée en chargeant une instruction en même temps. Il n'y a plus besoin de prefetch input queue, et c'est pour cette raison qu'elle a disparue sur le 486.
Pour les instructions simples, sans accès mémoire, le pipeline faisait seulement 4 à 5 étages :
- un étage pour le prefetch dans la PIQ ;
- un étage de décodage/prédécodage ;
- un étage pour le microcode ;
- un étage pour l'unité de calcul entière ;
- un étage pour l’enregistrement dans les registres.
Les multiplications/divisions et autres opérations complexes pouvaient rester plusieurs cycles dans l'unité de calcul. Heureusement, c'était des instructions microcodées. Le microcode se chargeait de bloquer les unités de chargement et décodage pendant ce temps, il agissait en quelque sorte comme une sorte d'unité d'émission. Les instructions flottantes ont elles un pipeline plus long, sans possibilité d'émettre des instructions entières en parallèle.
Pour les instructions avec un accès mémoire, les choses sont plus compliquées. Pour simplifier, il faut rajouter trois étages, dont deux pour la mémoire virtuelle : un pour la segmentation, un autre pour la pagination, un dernier pour l'accès au cache. Ils sont insérés entre l'étage pour l'unité de calcul entière et celui d'enregistrement dans les registres. L'étage avec l'ALU entière est utilisé partiellement pour les calculs d'adresse de type Base+indice, il est facultatif si le mode d'adressage n'utilise pas d'indice. Le résultat est donc :
- un étage pour le prefetch dans la PIQ ;
- un étage de décodage/prédécodage ;
- un étage pour le microcode ;
- un étage pour l'unité de calcul entière si mode adressage indicé ;
- un étage pour la segmentation ;
- un étage pour la pagination ;
- un étage pour l'accès au cache ou à la mémoire RAM ;
- un étage pour l’enregistrement dans les registres.
Un point important est que 486 intègre le contournement des lectures, à savoir qu'une donnée lue peut immédiatement être utilisée par l'ALU entière et/ou la FPU au cycle suivant. Cela permet de simplifier l'implémentation des instructions load-op, et améliore les performances en général. Par contre, l'optimisation d'early start du 386 n'est pas implémentée, car trop compliquée.
Les optimisations du bus mémoire sur le 486
[modifier | modifier le wikicode]L'intégration du cache a été complétée par de nombreuses optimisations de l'interface mémoire. Par exemple, le processeur peut effectuer des accès en rafale, chose que les EDO-DRAM de l'époque supportaient. Il avait aussi un write buffer qui mettait en attente 4 écritures, en sortie du cache, dans l'interface mémoire elle-même. Lors d'une écriture, le processeur écrivait dans le cache et dans ce write buffer, puis continuait son travail sans attendre que l'écriture soit faite en mémoire RAM. Les écritures sont faites en mémoire RAM quand le bus mémoire est libre. Le write buffer peut mettre 4 écritures en attente, mais le processeur stalle à la cinquième écriture.
Le processeur pouvait effectuer des lectures, même si des écritures sont en attente dans le write buffer. Mais il faut certaines conditions pour que cela ne cause aucun problème. Un processeur moderne ferait du réacheminement lecture sur écriture, à savoir que l'on comparerait la lecture avec les écritures en attente. Si l'adresse lu correspond à une adresse écrite, alors une dépendance est détectée. Mais le 486 utilisait une méthode plus simple, moins performante, mais qui utilise moins de circuits.
A la place, la lecture n'est exécutée en avance que si les écritures en attente sont des succès de cache. Je rappelle que le cache est de type write through. Si les écritures sont des succès de cache, cela veut dire que la donnée a déjà été écrite dans le cache, mais qu'elle attend de l'être en mémoire RAM. Faire une lecture en mémoire implique que celle-ci est un défaut de cache. Le fait d'avoir une lecture en défaut de cache et des écritures en succès de cache implique que la lecture n’accède pas aux données écrites. Cette contrainte garantit que la lecture n'a aucune dépendance avec les écritures en attente. Pas besoin de tester les dépendances, on a juste besoin de mémoriser pour chaque écriture si elle provient d'un succès ou défaut de cache, un bit suffit.
Par contre, faire une lecture en avance ainsi ne peut être fait qu'une seule fois. Les écritures en attente doivent être vidées en mémoire RAM pour que l'on puisse faire une nouvelle lecture en avance. C'est pour éviter qu'une seconde lecture mène à des dépendances mémoire particulières. Pour cela, quand une lecture est exécutée en avance, les écritures en attente sont toutes marquées comme des défauts de cache. D'autres circonstances demandent de marquer les écritures en attente comme des défauts de cache. Par exemple, toute invalidation du cache demande de le faire.
- Vous noterez que si le cache est désactivé, le write buffer est désactivé, lui aussi.
