Wikilivres
frwikibooks
https://fr.wikibooks.org/wiki/Accueil
MediaWiki 1.47.0-wmf.23
first-letter
Média
Spécial
Discussion
Utilisateur
Discussion utilisateur
Wikilivres
Discussion Wikilivres
Fichier
Discussion fichier
MediaWiki
Discussion MediaWiki
Modèle
Discussion modèle
Aide
Discussion aide
Catégorie
Discussion catégorie
Transwiki
Discussion Transwiki
Wikijunior
Discussion Wikijunior
TimedText
TimedText talk
Module
Discussion module
Event
Event talk
Discussion:Accueil
1
56846
774492
772361
2026-10-09T14:48:28Z
Lionel Scheepmans
20012
/* Je ne suis donc pas content tous seul. (2) */ Réponse
774492
wikitext
text/x-wiki
{{Avertissement|Cette page est réservée aux discussions au sujet de la page d'[[Accueil]]. Les discussions sur le projet Wikilivres en général ont lieux sur [[Wikilivres:Le Bistro|Le Bistro]]}}
{{RemarqueIndex|Pour la maintenance, voir [[Wikilivres:Maintenance/Page d'accueil|Maintenance de la page d'accueil]].}}
*[[Discuter:Accueil/Archive 1|Archive ==>mars 2009]]
*[[Discuter:Accueil/Archive 2|Archive ==>mai 2013]]
== Chiffres ==
Pourquoi ne publie-t-on pas comme sur les autres wikis une indication de l'importance du site, avec quelque chose comme :
; {{PAGESINCATEGORY:Livres par titre}} livres sur {{NUMBEROFARTICLES}} pages
? [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<font color="#FF6600">$</font>♠]]) 16 août 2013 à 22:21 (CEST)
:Je pense qu'il s'agit d'afficher des stats sur la page d'accueil, comme sur celle de Wikipedia tout en haut à droite ou dans le bistro du jour. Pourquoi pas? ''Liebe Grüße'', [[Utilisateur:Perditax|Perditax]] ([[Discussion utilisateur:Perditax|d]]) 19 septembre 2013 à 13:45 (CEST)
:: Pour ! [[Utilisateur:Jean-Jacques MILAN|Jean-Jacques MILAN]] ([[Discussion utilisateur:Jean-Jacques MILAN|discussion]]) 22 septembre 2013 à 13:51 (CEST)
== Auteur ???? ==
Et si on cherche livres d'un auteur specifique ?
???
:Les auteurs de Wikilivres qui le souhaitent peuvent publier leur bibliographie sur leur profil. Quant aux auteurs célèbres, ils sont sur [[s:|Wikisource]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<font color="#FF6600">$</font>♠]]) 18 juillet 2015 à 16:56 (CEST)
== Version mobile ==
Bonjour,
Je pense que ce serait une bonne idée de mettre le lien de la version mobile de Wikibooks pour que celui qui utilise un téléphone ou une tablette ne soit pas perdu et ne cherche pas n'importe où comment avoir un affichage correct.
Merci, [[Utilisateur:Aabbccddeeffabcdef|Aabbccddeeffabcdef]] ([[Discussion utilisateur:Aabbccddeeffabcdef|discussion]]) 19 décembre 2018 à 15:39 (CET).
:Les navigateurs des téléphones basculent automatiquement dessus par défaut. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 19 décembre 2018 à 23:54 (CET)
== Rénovation ==
Bonjour,
Selon le projet [[w:fr:Projet:Charte graphique|Charte graphique]] de Wikipédia, pourrait-t-on rénover la page d'accueil avec les modèles [[Modèle:Boîte colorée|Boîte colorée]] et [[Modèle:Barre colorée|Barre colorée]].
Merci & Cordialement, [[Utilisateur:Aabbccddeeffabcdef|Aabbccddeeffabcdef]] ([[Discussion utilisateur:Aabbccddeeffabcdef|discussion]]), le [[Utilisateur:Aabbccddeeffabcdef|Aabbccddeeffabcdef]] ([[Discussion utilisateur:Aabbccddeeffabcdef|discussion]]) 13 mars 2019 à 17:38 (CET).
:Les rendus des versions desktop et mobile me semblent corrects aujourd'hui, donc je ne souhaiterais pas toucher à quelque chose qui fonctionne. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 mars 2019 à 20:52 (CET)
== Faute d'orthographe sur [[Ict@innovation: Free your IT Business in Africa/2-1]] ==
Il y a une faute d'orthographe sur [[Ict@innovation: Free your IT Business in Africa/2-1]], mais dès que je veux modifier cette page, je suis redirigé vers la page d'accueil. J'ai corrigé d'innombrables pages sur wikipedia et les autres wiki cousins, mais là : impossible.
Je veux juste remplacer "été crée" par "été créé".
[[Utilisateur:Romanc19s|Romanc19s]] ([[Discussion utilisateur:Romanc19s|discussion]]) 4 mars 2021 à 19:03 (CET)
:Salut Romanc19s
:Effectivement, il y a un script qui bloque la création et la modification des pages ayant un nom bizarre.
:La page ne peut avoir été créée que par un bot (javascript ignoré) ou un administrateur.
:J'ai effectué la correction. Mais on peut remettre en cause la pertinence de la présence d'une telle page.
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 4 mars 2021 à 19:49 (CET)
== Connexion ==
Pourquoi ne puis-je plus me connecter sous mon autre nom ? --[[Utilisateur:Elnon|Elnon]] ([[Discussion utilisateur:Elnon|discussion]]) 9 juillet 2026 à 21:59 (CEST)
== Un changement de la page d'Accueil ==
J'ai enclenché un changement de la page d'Accueil pour éviter aux lecteurs de se perdre dans les méandres de Wikibook.
Je pense que les liens "Livres terminés" - "Mini Livres" font bien leur travail. Dans les nouveautés j'ai proposé de laisser les nouveaux livres pendant 1 mois, mais de permettre aux auteurs actifs de laisser leurs livres pendant plusieurs mois, voire plusieurs années tant qu'ils n'estimeront pas leurs livres terminés.
Il y a toujours un lien de déperdition qui permet de se perdre dans les méandres de Wikilivre. Où est '''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil l'espace pour les lecteurs ?]''' [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 16 septembre 2026 à 15:05 (CEST)
== Usage du Wikicode ==
@[[Utilisateur:DavidL|DavidL]] et @[[Utilisateur:JackPotte|JackPotte]]. Je suis en train de refaire la mise en page de notre page d'accueil suite à la décision initiée par
[[Utilisateur:Xhungab|Xhungab]]. J'en profite aussi pour traiter les problèmes d'affichage par les smartphones. Le code de cette page utilise beaucoup de code CSS, voire HTML. C'est sans doute très commode pour les personnes comme vous qui connaissent bien ce langage, mais j'y vois un inconvénient.
Cela rend les choses plus difficiles à comprendre et à modifier pour des gens comme moi qui ne maîtrisent pas le CSS et l'HTML, contrairement au Wikicode.
Le Wikicode a été créé pour être plus facile d'usage et je me dis qu'il sera aussi toujours pris en charge par les futurs contributeurs qui ne sont pas informaticiens. De plus, tout ce qui est écrit en Wikicode sera toujours pris en charge dans le cadre des améliorations faites par le personnel de la fondation.
Je propose donc de réécrire le code de la page d'accueil en utilisant au maximum le Wikicode et aucun code HTML ou CSS si possible.
Qu'en pensez-vous ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 septembre 2026 à 15:06 (CEST)
== Problème des cadre et tableau en version mobile ==
Je découvre que l'usage de cadre et de tableau contrarie l'affichage en version mobile. Je tenterai de trouver une solution. N'hésitez pas à réagir. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 septembre 2026 à 15:59 (CEST)
== Je ne suis donc pas content tous seul. (2) ==
'''Suite à l'installation de la nouvelle page de la Vitrine, je ne suis pas content, j'ai pris 24h pour réfléchir.
'''
J'avais posé une demande de prise de décision concernant le sujet suivant.
'''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil Créer un espace lecteurs et un espace auteurs sur la page d'accueil]'''
'''Créer un espace lecteurs''' : Où est l'espace pour les lecteurs?
Découvrir la bibliothèque
Livres terminés - Mini Livres - Tous les livres
'''Dans une bibliothèque''', un livre est disponible ou indisponible. C'est chez l'auteur ou chez l'imprimeur qu'un livre est terminé.
'''Je suis un nouveau sur Wikilivre'''. Je vois, "Découvrir la bibliothèque", je vais directement dans "'''[[Wikilivres:Tous les livres|Tous les livres]]'''". Je me retrouve dans un labyrinthe qui me balade de répertoires en répertoires pour finir sur une ébauche abandonnée depuis 5, 10, 20 ans.
"Tous les livres" devraient s'appeler la réserve de Wikibook. C'est l'endroit fermé au public où sont conservés les livres et les documents qui ne sont pas en accès libre dans les salles de lecture. C'est un espace pour les curieux pour connaître les entrailles de Wikibook.
Je ne suis pas content. J'avais proposé à l'administrateur un changement en 2 minutes. Copier cette page dans la Vitrine, [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab La Nouvelle Vitrine]. Il a décidé de passer plusieurs heures à modifier la prise de décision. Je ne suis pas content. Je suppose que je suis le seul.
Donc : '''Je ne suis donc pas content tous seul.''' mais je me soigne. Un pan bagnat et une tourte aux blettes.
Merci de m'avoir lu [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 septembre 2026 à 10:51 (CEST)
:: Je replace ici un message précédent que j'avais maladroitement supprimé :
::: Bonjour @[[Utilisateur:Xhungab|Xhungab]].
::: Nous tentons de fonctionner au consensus dans les projets Wikimédia. On essaie donc de faire les choses avec un consentement général. La prise de décision a été rapide, les discussions inachevées, et certaines personnes n'étaient pas satisfaite de tes propositions. Comme la cloture de la décision a été faite, je me suis mis au travail.
::: Il est ensuite important de signaler que dans [[Discussion utilisateur:Lionel Scheepmans#Décision:Prise de décision/Créer un espace lecteurs et un espace auteurs sur la page d'accueil|une précédente discussion]], je suis resté ouvert à tout changement de la page d'accueil à partir de ce que j'ai considéré être le plus rationnel au regard de tout ce qui existait déjà. C'est-à-dire [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Index?prefix=&namespace=4 de nombreuses pages] créées et améliorées par le passé que nous ne pouvons pas ignorer au risque de créer des doublons.
::: Si tu es insatisfait du résultat, je te propose de nous retrouver, comme cela a déjà été proposé aussi dans notre précédente discussion, sur la [[Discussion:Accueil|page de discussion de la page d'accueil]] qui est le meilleurs endroit pour discuter de son contenu tout en étant sûr que ces discussion ne se perdent pas dans un archivage, comme c'est le cas pour le bistro. On peut donc s'y retrouver, avec toutes les autres personnes qui ont des propositions d'améliorations.
::: Du reste, nous sommes tous bénévoles ici. Y compris les administrateurs. Pour ma part, être bénévole, c'est faire les choses par sa propre volonté et avec bienveillance. C'est être volontaire bienveillant. Pour ma part, je reste toujours bienveillant. Ma volonté, quant à elle, peut changer en fonction de mes convictions et motivations.
::: Une belle fin de journée à tous ! [[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[Discussion utilisateur:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 17 septembre 2026 à 13:20 (CEST)
lhmb80le3k335ly6x85dtbzw9p25bs7
Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)
0
65770
774570
738090
2026-10-09T23:34:49Z
Mewtow
31375
774570
wikitext
text/x-wiki
Tout ordinateur contient au minimum un bus, qui sert à connecter processeur, mémoire et entrées-sorties. Mais c'est là le cas le plus simple : rien n’empêche un ordinateur d'avoir plusieurs bus : un bus pour communiquer avec le disque dur, un bus pour la carte graphique, un pour le processeur, un pour la mémoire, etc. De ce fait, un PC moderne contient un nombre impressionnant de bus, jugez plutôt :
* les bus USB ;
* le bus PCI Express, utilisé pour communiquer avec des cartes graphiques ou des cartes son ;
* le bus S-ATA et ses variantes eSATA, eSATAp, ATAoE, utilisés pour communiquer avec le disque dur ;
* le bus Low Pin Count, qui permet d'accéder au clavier, aux souris, au lecteur de disquette, et aux ports parallèle et série ;
* le SMBUS, qui est utilisé pour communiquer avec les ventilateurs, les sondes de température et les sondes de tension présentes un peu partout dans notre ordinateur ;
* l'Intel QuickPath Interconnect et l'HyperTransport, qui relient les processeurs récents au reste de l'ordinateur ;
Et c'est oublier tous les bus aujourd'hui défunts, mais qui étaient utilisés sur les anciens PC. Comme exemples, on pourrait citer :
* le bus ISA et le bus PCI (l'ancêtre du PCI Express), autrefois utilisés pour les cartes d'extension ;
* le bus AGP, autrefois utilisé pour les cartes graphiques ;
* les bus P-ATA et SCSI, pour les disque durs ;
* le bus MIDI, qui servait pour les cartes son ;
* le fameux RS-232 utilisé dans les ports série ;
* enfin, le bus IEEE-1284 utilisé pour le port parallèle.
Et à ces bus reliés aux périphériques, il faudrait rajouter le bus mémoire qui connecte processeur et mémoire, le bus système et bien d'autres. La longue liste précédente sous-entend qu'il existe de nombreuses différences entre les bus. Et c'est le cas : ces différents bus sont très différents les uns des autres.
==Les bus et liaisons point à point==
Commençons par parler de la distinction entre les bus et les liaisons point à point. La différence est qu'une liaison point à point relie deux composants, alors qu'un bus connecte au moins trois composants, souvent plus.
: ''Petite précision de vocabulaire : Le composant qui envoie une donnée sur un bus est appelé un émetteur, alors que ceux reçoivent les données sont appelés récepteurs.''
===Les liaisons point à point===
Les '''liaisons point à point''' connectent deux composants entre eux. Pour en donner un exemple, le câble réseau qui relie votre ordinateur à votre box internet est une liaison point à point. Mais le terme est plus large que cela et regroupe tout ce qui connecte deux équipements informatiques/électroniques entre eux, pour les faire échanger des données. Par exemple, le câble qui relie votre ordinateur à votre imprimante est lui aussi une liaison point à point, au même titre que les liaisons USB sur votre ordinateur.
Mais ce qui va nous intéresser, ce sont les liaisons point à point qui relient des composants à l'intérieur d'un ordinateur, comme le processeur et certains capteurs de températures.
Les liaisons point à point sont classés en trois types, suivant les récepteurs et les émetteurs.
{|class="wikitable"
|-
!Type de bus
!Description
|-
!''Simplex''
|Les informations ne vont que dans un sens : un composant est l'émetteur et l'autre reste à tout jamais récepteur.
|-
!''Half-duplex''
|Il est possible d'être émetteur ou récepteur, suivant la situation. Par contre, impossible d'être en même temps émetteur et récepteur.
|-
!''Full-duplex''
|Il est possible d'être à la fois récepteur et émetteur.
|}
{|class="wikitable"
|-
!Liaison ''simplex''
!Liaison ''half-duplex''
!Liaison ''full-duplex''
|-
|[[File:Simplex.jpg|250px]]
|[[File:HalfDuplex.JPG|250px]]
|[[File:FullDuplex.JPG|250px]]
|}
Les liaisons ''full duplex'' sont créés en regroupant deux liaisons simplex ensemble : une pour l'émission et une pour la réception. Mais certaines liaisons ''full-duplex'' n'utilisent pas cette technique et se contentent d'un seul bus bidirectionnel.
===Les bus 'multiplexés)===
Les liaisons point à point sont à opposer aux bus proprement dit, aussi appelés '''bus multiplexés'''. Ces derniers ne sont pas limités à deux composants et peuvent interconnecter un grand nombre de circuits électroniques. Par exemple, un bus peut interconnecter la mémoire RAM, le processeur et quelques entrées-sorties entre eux. Et cela fait qu'il existe quelques différences entre un bus et une liaison point à point.
[[File:Intercpunetbus.png|centre|vignette|upright=2|Bus]]
Avec un bus, l'émetteur envoie ses données à tous les autres composants reliés aux bus, à tous les récepteurs. Sur tous ces récepteurs, il se peut que seul l'un d'entre eux soit le destinataire de la donnée : les autres vont alors l'ignorer, seul le destinataire la traite. Cependant, il se peut qu'il y ait plusieurs récepteurs comme destinataires : dans ce cas, les destinataires vont tous recevoir la donnée et la traiter. Les bus permettent donc de faire des envois de données à plusieurs composants en une seule fois.
[[File:ARINC429 Architecture Emetteur Recepteurs.svg|centre|vignette|upright=2|Bus multiplexés.]]
==La fréquence du bus et son caractère synchrone/asynchrone==
On peut faire la différence entre bus synchrone et asynchrone, la différence se faisant selon l'usage ou non d'une horloge. La méthode de synchronisation des composants et des communications sur le bus peut ainsi utiliser une horloge, ou la remplacer par des mécanismes autres.
===Les bus synchrones===
La grande majorité des bus actuellement utilisés sont synchronisés sur un signal d'horloge : ce sont les '''bus synchrones'''. Avec ces bus, le temps de transmission d'une donnée est fixé une fois pour toute. Le composant sait combien de cycles d'horloge durent une lecture ou une écriture. Sur certains bus, le contenu du bus n'est pas mis à jour à chaque front montant, ou à chaque front descendant, mais aux deux : fronts montant et descendant. De tels bus sont appelés des bus double data rate. Cela permet de transférer deux données sur le bus (une à chaque front) en un seul cycle d'horloge : le débit binaire est doublé sans toucher à la fréquence du bus.
[[File:Exemple de lecture sur un bus synchrone.png|centre|vignette|upright=2|Exemple de lecture sur un bus synchrone.]]
===Les bus asynchrones===
Une minorité de bus se passent complètement de signal d'horloge, et ont un protocole conçu pour : ce sont les '''bus asynchrones'''. Les bus asynchrones utilisent des protocoles de communication spécialisés. Les bus asynchrones permettent à deux circuits/composants de se synchroniser, l'un des deux étant un émetteur, l'autre étant un récepteur. Pour se synchroniser, l’émetteur indique au récepteur qu'il lui a envoyé une donnée, généralement grâce à un bit dédié sur le bus, souvent appelé le bit REQ. Le récepteur réceptionne alors la donnée et indique qu'il a pris en compte les données envoyées en envoyant un bit ACK. Cette synchronisation se fait grâce à des fils spécialisés du bus de commande, qui transmettent des bits particuliers.
[[File:Exemple d'écriture sur un bus asynchrone.png|centre|vignette|upright=2|Exemple d'écriture sur un bus asynchrone]]
De tels bus sont donc très adaptés pour transmettre des informations sur de longues distances (plusieurs centimètres ou plus). La raison est qu'à haute fréquence, le signal d'horloge met un certain temps pour se propager à travers le fil d'horloge, ce qui induit un léger décalage entre les composants. Plus on augmente la longueur des fils, plus ces décalages deviendront ennuyeux. Plus on augmente la fréquence, plus la période est dominée par le temps de propagation de l'horloge dans le fil. Les bus asynchrones n'ont pas ce genre de problèmes.
==La largeur du bus==
[[File:Parallel and Serial Transmission.gif|vignette|Comparaison entre bus série et parallèle.]]
La plupart des bus peuvent échanger plusieurs bits en même temps et sont appelés '''bus parallèles'''. Mais il existe des bus qui ne peuvent échanger qu'un bit à la fois : ce sont des '''bus série'''.
===Les bus série===
On pourrait croire qu'un bus série ne contient qu'un seul fil pour transmettre les données, mais il existe des contrexemples. Généralement, c'est le signe que le bus n'utilise pas un codage NRZ, mais une autre forme de codage un peu plus complexe. Par exemple, le bus USB utilise deux fils D+ et D- pour transmettre un bit. Pour faire simple, lorsque le fil D+ est à sa tension maximale, l'autre est à zéro (et réciproquement).
La transmission et la réception sur un bus série demande de faire une conversion entre les données, qui sont codées sur plusieurs bits, et le flux série à envoyer sur le bus. Cela s'effectue généralement en utilisant des registres à décalage, commandés par des circuits de contrôle.
[[File:Schema uart.png|centre|vignette|upright=2.0|Interface de conversion série-parallèle (UART).]]
===Les bus parallèles===
Passons maintenant aux bus parallèles. Pour information, si le contenu d'un bus de largeur de <math>L</math> bits est mis à jour <math>F</math> fois par secondes, alors son débit binaire est de <math>L \times F</math>. Mais contrairement à ce qu'on pourrait croire, les bus parallèles ne sont pas plus rapides que les bus série. Sur les bus synchrones, la fréquence est bien meilleure pour les bus série que pour les bus parallèles. La fréquence plus élevée l'emporte sur la largeur plus faible, ce qui surcompense le fait qu’un bus série ne peut envoyer qu'un bit à la fois. Le même problème se pose pour les bus asynchrones : le temps entre deux transmissions est plus grand sur les bus parallèles, alors qu'un bus série n'a pas ce genre de problèmes.
Il existe plusieurs raisons à cela, qui proviennent de phénomènes électriques assez subtils. Premièrement, les fils d'un bus ne sont pas identiques électriquement : leur longueur et leur résistance changent très légèrement d'un fil à l'autre. En conséquence, un bit va se propager à des vitesses différentes suivant le fil. On est obligé de se caler sur le fil le plus lent pour éviter des problèmes à la réception. En second lieu, il y a le phénomène de ''crosstalk''. Lorsque la tension à l'intérieur du fil varie (quand le fil passe de 0 à 1 ou inversement), le fil va émettre des ondes électromagnétiques qui perturbent les fils d'à côté. Il faut attendre que la perturbation électromagnétique se soit atténuée pour lire les bits, ce qui limite le nombre de changements d'état du bus par seconde.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La carte mère, chipset et BIOS
| prevText=La carte mère, chipset et BIOS
| next=Les encodages spécifiques aux bus
| nextText=Les encodages spécifiques aux bus
}}{{autocat}}
</noinclude>
68qcbf5t6qn9qytb6wk3x88elukrxhc
774571
774570
2026-10-09T23:36:00Z
Mewtow
31375
/* Les bus et liaisons point à point */
774571
wikitext
text/x-wiki
Tout ordinateur contient au minimum un bus, qui sert à connecter processeur, mémoire et entrées-sorties. Mais c'est là le cas le plus simple : rien n’empêche un ordinateur d'avoir plusieurs bus : un bus pour communiquer avec le disque dur, un bus pour la carte graphique, un pour le processeur, un pour la mémoire, etc. De ce fait, un PC moderne contient un nombre impressionnant de bus, jugez plutôt :
* les bus USB ;
* le bus PCI Express, utilisé pour communiquer avec des cartes graphiques ou des cartes son ;
* le bus S-ATA et ses variantes eSATA, eSATAp, ATAoE, utilisés pour communiquer avec le disque dur ;
* le bus Low Pin Count, qui permet d'accéder au clavier, aux souris, au lecteur de disquette, et aux ports parallèle et série ;
* le SMBUS, qui est utilisé pour communiquer avec les ventilateurs, les sondes de température et les sondes de tension présentes un peu partout dans notre ordinateur ;
* l'Intel QuickPath Interconnect et l'HyperTransport, qui relient les processeurs récents au reste de l'ordinateur ;
Et c'est oublier tous les bus aujourd'hui défunts, mais qui étaient utilisés sur les anciens PC. Comme exemples, on pourrait citer :
* le bus ISA et le bus PCI (l'ancêtre du PCI Express), autrefois utilisés pour les cartes d'extension ;
* le bus AGP, autrefois utilisé pour les cartes graphiques ;
* les bus P-ATA et SCSI, pour les disque durs ;
* le bus MIDI, qui servait pour les cartes son ;
* le fameux RS-232 utilisé dans les ports série ;
* enfin, le bus IEEE-1284 utilisé pour le port parallèle.
Et à ces bus reliés aux périphériques, il faudrait rajouter le bus mémoire qui connecte processeur et mémoire, le bus système et bien d'autres. La longue liste précédente sous-entend qu'il existe de nombreuses différences entre les bus. Et c'est le cas : ces différents bus sont très différents les uns des autres.
==Les bus et liaisons point à point==
Commençons par parler de la distinction entre les bus et les liaisons point à point. La différence est qu'une liaison point à point relie deux composants, alors qu'un bus connecte au moins trois composants, souvent plus. Nous ferons parfois la confusion entre les deux, et regrouperons parfois les deux sous le terme "bus", sans plus de précisions. Il faut dire qu'une bonne partie de ce qui est valable pour les liaisons point à point l'est aussi pour les bus.
: ''Petite précision de vocabulaire : Le composant qui envoie une donnée sur un bus est appelé un émetteur, alors que ceux reçoivent les données sont appelés récepteurs.''
===Les liaisons point à point===
Les '''liaisons point à point''' connectent deux composants entre eux. Pour en donner un exemple, le câble réseau qui relie votre ordinateur à votre box internet est une liaison point à point. Mais le terme est plus large que cela et regroupe tout ce qui connecte deux équipements informatiques/électroniques entre eux, pour les faire échanger des données. Par exemple, le câble qui relie votre ordinateur à votre imprimante est lui aussi une liaison point à point, au même titre que les liaisons USB sur votre ordinateur.
Mais ce qui va nous intéresser, ce sont les liaisons point à point qui relient des composants à l'intérieur d'un ordinateur, comme le processeur et certains capteurs de températures.
Les liaisons point à point sont classés en trois types, suivant les récepteurs et les émetteurs.
{|class="wikitable"
|-
!Type de bus
!Description
|-
!''Simplex''
|Les informations ne vont que dans un sens : un composant est l'émetteur et l'autre reste à tout jamais récepteur.
|-
!''Half-duplex''
|Il est possible d'être émetteur ou récepteur, suivant la situation. Par contre, impossible d'être en même temps émetteur et récepteur.
|-
!''Full-duplex''
|Il est possible d'être à la fois récepteur et émetteur.
|}
{|class="wikitable"
|-
!Liaison ''simplex''
!Liaison ''half-duplex''
!Liaison ''full-duplex''
|-
|[[File:Simplex.jpg|250px]]
|[[File:HalfDuplex.JPG|250px]]
|[[File:FullDuplex.JPG|250px]]
|}
Les liaisons ''full duplex'' sont créés en regroupant deux liaisons simplex ensemble : une pour l'émission et une pour la réception. Mais certaines liaisons ''full-duplex'' n'utilisent pas cette technique et se contentent d'un seul bus bidirectionnel.
===Les bus 'multiplexés)===
Les liaisons point à point sont à opposer aux bus proprement dit, aussi appelés '''bus multiplexés'''. Ces derniers ne sont pas limités à deux composants et peuvent interconnecter un grand nombre de circuits électroniques. Par exemple, un bus peut interconnecter la mémoire RAM, le processeur et quelques entrées-sorties entre eux. Et cela fait qu'il existe quelques différences entre un bus et une liaison point à point.
[[File:Intercpunetbus.png|centre|vignette|upright=2|Bus]]
Avec un bus, l'émetteur envoie ses données à tous les autres composants reliés aux bus, à tous les récepteurs. Sur tous ces récepteurs, il se peut que seul l'un d'entre eux soit le destinataire de la donnée : les autres vont alors l'ignorer, seul le destinataire la traite. Cependant, il se peut qu'il y ait plusieurs récepteurs comme destinataires : dans ce cas, les destinataires vont tous recevoir la donnée et la traiter. Les bus permettent donc de faire des envois de données à plusieurs composants en une seule fois.
[[File:ARINC429 Architecture Emetteur Recepteurs.svg|centre|vignette|upright=2|Bus multiplexés.]]
==La fréquence du bus et son caractère synchrone/asynchrone==
On peut faire la différence entre bus synchrone et asynchrone, la différence se faisant selon l'usage ou non d'une horloge. La méthode de synchronisation des composants et des communications sur le bus peut ainsi utiliser une horloge, ou la remplacer par des mécanismes autres.
===Les bus synchrones===
La grande majorité des bus actuellement utilisés sont synchronisés sur un signal d'horloge : ce sont les '''bus synchrones'''. Avec ces bus, le temps de transmission d'une donnée est fixé une fois pour toute. Le composant sait combien de cycles d'horloge durent une lecture ou une écriture. Sur certains bus, le contenu du bus n'est pas mis à jour à chaque front montant, ou à chaque front descendant, mais aux deux : fronts montant et descendant. De tels bus sont appelés des bus double data rate. Cela permet de transférer deux données sur le bus (une à chaque front) en un seul cycle d'horloge : le débit binaire est doublé sans toucher à la fréquence du bus.
[[File:Exemple de lecture sur un bus synchrone.png|centre|vignette|upright=2|Exemple de lecture sur un bus synchrone.]]
===Les bus asynchrones===
Une minorité de bus se passent complètement de signal d'horloge, et ont un protocole conçu pour : ce sont les '''bus asynchrones'''. Les bus asynchrones utilisent des protocoles de communication spécialisés. Les bus asynchrones permettent à deux circuits/composants de se synchroniser, l'un des deux étant un émetteur, l'autre étant un récepteur. Pour se synchroniser, l’émetteur indique au récepteur qu'il lui a envoyé une donnée, généralement grâce à un bit dédié sur le bus, souvent appelé le bit REQ. Le récepteur réceptionne alors la donnée et indique qu'il a pris en compte les données envoyées en envoyant un bit ACK. Cette synchronisation se fait grâce à des fils spécialisés du bus de commande, qui transmettent des bits particuliers.
[[File:Exemple d'écriture sur un bus asynchrone.png|centre|vignette|upright=2|Exemple d'écriture sur un bus asynchrone]]
De tels bus sont donc très adaptés pour transmettre des informations sur de longues distances (plusieurs centimètres ou plus). La raison est qu'à haute fréquence, le signal d'horloge met un certain temps pour se propager à travers le fil d'horloge, ce qui induit un léger décalage entre les composants. Plus on augmente la longueur des fils, plus ces décalages deviendront ennuyeux. Plus on augmente la fréquence, plus la période est dominée par le temps de propagation de l'horloge dans le fil. Les bus asynchrones n'ont pas ce genre de problèmes.
==La largeur du bus==
[[File:Parallel and Serial Transmission.gif|vignette|Comparaison entre bus série et parallèle.]]
La plupart des bus peuvent échanger plusieurs bits en même temps et sont appelés '''bus parallèles'''. Mais il existe des bus qui ne peuvent échanger qu'un bit à la fois : ce sont des '''bus série'''.
===Les bus série===
On pourrait croire qu'un bus série ne contient qu'un seul fil pour transmettre les données, mais il existe des contrexemples. Généralement, c'est le signe que le bus n'utilise pas un codage NRZ, mais une autre forme de codage un peu plus complexe. Par exemple, le bus USB utilise deux fils D+ et D- pour transmettre un bit. Pour faire simple, lorsque le fil D+ est à sa tension maximale, l'autre est à zéro (et réciproquement).
La transmission et la réception sur un bus série demande de faire une conversion entre les données, qui sont codées sur plusieurs bits, et le flux série à envoyer sur le bus. Cela s'effectue généralement en utilisant des registres à décalage, commandés par des circuits de contrôle.
[[File:Schema uart.png|centre|vignette|upright=2.0|Interface de conversion série-parallèle (UART).]]
===Les bus parallèles===
Passons maintenant aux bus parallèles. Pour information, si le contenu d'un bus de largeur de <math>L</math> bits est mis à jour <math>F</math> fois par secondes, alors son débit binaire est de <math>L \times F</math>. Mais contrairement à ce qu'on pourrait croire, les bus parallèles ne sont pas plus rapides que les bus série. Sur les bus synchrones, la fréquence est bien meilleure pour les bus série que pour les bus parallèles. La fréquence plus élevée l'emporte sur la largeur plus faible, ce qui surcompense le fait qu’un bus série ne peut envoyer qu'un bit à la fois. Le même problème se pose pour les bus asynchrones : le temps entre deux transmissions est plus grand sur les bus parallèles, alors qu'un bus série n'a pas ce genre de problèmes.
Il existe plusieurs raisons à cela, qui proviennent de phénomènes électriques assez subtils. Premièrement, les fils d'un bus ne sont pas identiques électriquement : leur longueur et leur résistance changent très légèrement d'un fil à l'autre. En conséquence, un bit va se propager à des vitesses différentes suivant le fil. On est obligé de se caler sur le fil le plus lent pour éviter des problèmes à la réception. En second lieu, il y a le phénomène de ''crosstalk''. Lorsque la tension à l'intérieur du fil varie (quand le fil passe de 0 à 1 ou inversement), le fil va émettre des ondes électromagnétiques qui perturbent les fils d'à côté. Il faut attendre que la perturbation électromagnétique se soit atténuée pour lire les bits, ce qui limite le nombre de changements d'état du bus par seconde.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La carte mère, chipset et BIOS
| prevText=La carte mère, chipset et BIOS
| next=Les encodages spécifiques aux bus
| nextText=Les encodages spécifiques aux bus
}}{{autocat}}
</noinclude>
kie5lk3sjnqfk75dr3yoat6xe7teuzm
774572
774571
2026-10-09T23:37:55Z
Mewtow
31375
/* La fréquence du bus et son caractère synchrone/asynchrone */
774572
wikitext
text/x-wiki
Tout ordinateur contient au minimum un bus, qui sert à connecter processeur, mémoire et entrées-sorties. Mais c'est là le cas le plus simple : rien n’empêche un ordinateur d'avoir plusieurs bus : un bus pour communiquer avec le disque dur, un bus pour la carte graphique, un pour le processeur, un pour la mémoire, etc. De ce fait, un PC moderne contient un nombre impressionnant de bus, jugez plutôt :
* les bus USB ;
* le bus PCI Express, utilisé pour communiquer avec des cartes graphiques ou des cartes son ;
* le bus S-ATA et ses variantes eSATA, eSATAp, ATAoE, utilisés pour communiquer avec le disque dur ;
* le bus Low Pin Count, qui permet d'accéder au clavier, aux souris, au lecteur de disquette, et aux ports parallèle et série ;
* le SMBUS, qui est utilisé pour communiquer avec les ventilateurs, les sondes de température et les sondes de tension présentes un peu partout dans notre ordinateur ;
* l'Intel QuickPath Interconnect et l'HyperTransport, qui relient les processeurs récents au reste de l'ordinateur ;
Et c'est oublier tous les bus aujourd'hui défunts, mais qui étaient utilisés sur les anciens PC. Comme exemples, on pourrait citer :
* le bus ISA et le bus PCI (l'ancêtre du PCI Express), autrefois utilisés pour les cartes d'extension ;
* le bus AGP, autrefois utilisé pour les cartes graphiques ;
* les bus P-ATA et SCSI, pour les disque durs ;
* le bus MIDI, qui servait pour les cartes son ;
* le fameux RS-232 utilisé dans les ports série ;
* enfin, le bus IEEE-1284 utilisé pour le port parallèle.
Et à ces bus reliés aux périphériques, il faudrait rajouter le bus mémoire qui connecte processeur et mémoire, le bus système et bien d'autres. La longue liste précédente sous-entend qu'il existe de nombreuses différences entre les bus. Et c'est le cas : ces différents bus sont très différents les uns des autres.
==Les bus et liaisons point à point==
Commençons par parler de la distinction entre les bus et les liaisons point à point. La différence est qu'une liaison point à point relie deux composants, alors qu'un bus connecte au moins trois composants, souvent plus. Nous ferons parfois la confusion entre les deux, et regrouperons parfois les deux sous le terme "bus", sans plus de précisions. Il faut dire qu'une bonne partie de ce qui est valable pour les liaisons point à point l'est aussi pour les bus.
: ''Petite précision de vocabulaire : Le composant qui envoie une donnée sur un bus est appelé un émetteur, alors que ceux reçoivent les données sont appelés récepteurs.''
===Les liaisons point à point===
Les '''liaisons point à point''' connectent deux composants entre eux. Pour en donner un exemple, le câble réseau qui relie votre ordinateur à votre box internet est une liaison point à point. Mais le terme est plus large que cela et regroupe tout ce qui connecte deux équipements informatiques/électroniques entre eux, pour les faire échanger des données. Par exemple, le câble qui relie votre ordinateur à votre imprimante est lui aussi une liaison point à point, au même titre que les liaisons USB sur votre ordinateur.
Mais ce qui va nous intéresser, ce sont les liaisons point à point qui relient des composants à l'intérieur d'un ordinateur, comme le processeur et certains capteurs de températures.
Les liaisons point à point sont classés en trois types, suivant les récepteurs et les émetteurs.
{|class="wikitable"
|-
!Type de bus
!Description
|-
!''Simplex''
|Les informations ne vont que dans un sens : un composant est l'émetteur et l'autre reste à tout jamais récepteur.
|-
!''Half-duplex''
|Il est possible d'être émetteur ou récepteur, suivant la situation. Par contre, impossible d'être en même temps émetteur et récepteur.
|-
!''Full-duplex''
|Il est possible d'être à la fois récepteur et émetteur.
|}
{|class="wikitable"
|-
!Liaison ''simplex''
!Liaison ''half-duplex''
!Liaison ''full-duplex''
|-
|[[File:Simplex.jpg|250px]]
|[[File:HalfDuplex.JPG|250px]]
|[[File:FullDuplex.JPG|250px]]
|}
Les liaisons ''full duplex'' sont créés en regroupant deux liaisons simplex ensemble : une pour l'émission et une pour la réception. Mais certaines liaisons ''full-duplex'' n'utilisent pas cette technique et se contentent d'un seul bus bidirectionnel.
===Les bus 'multiplexés)===
Les liaisons point à point sont à opposer aux bus proprement dit, aussi appelés '''bus multiplexés'''. Ces derniers ne sont pas limités à deux composants et peuvent interconnecter un grand nombre de circuits électroniques. Par exemple, un bus peut interconnecter la mémoire RAM, le processeur et quelques entrées-sorties entre eux. Et cela fait qu'il existe quelques différences entre un bus et une liaison point à point.
[[File:Intercpunetbus.png|centre|vignette|upright=2|Bus]]
Avec un bus, l'émetteur envoie ses données à tous les autres composants reliés aux bus, à tous les récepteurs. Sur tous ces récepteurs, il se peut que seul l'un d'entre eux soit le destinataire de la donnée : les autres vont alors l'ignorer, seul le destinataire la traite. Cependant, il se peut qu'il y ait plusieurs récepteurs comme destinataires : dans ce cas, les destinataires vont tous recevoir la donnée et la traiter. Les bus permettent donc de faire des envois de données à plusieurs composants en une seule fois.
[[File:ARINC429 Architecture Emetteur Recepteurs.svg|centre|vignette|upright=2|Bus multiplexés.]]
==La fréquence du bus==
On peut faire la différence entre bus synchrone et asynchrone, la différence étant que les premiers utilisent une horloge, pas les autres.
===Les bus synchrones===
La grande majorité des bus actuellement utilisés sont synchronisés sur un signal d'horloge : ce sont les '''bus synchrones'''. Avec ces bus, le temps de transmission d'une donnée est fixé une fois pour toute. Le composant sait combien de cycles d'horloge durent une lecture ou une écriture.
[[File:Exemple de lecture sur un bus synchrone.png|centre|vignette|upright=2|Exemple de lecture sur un bus synchrone.]]
Sur certains bus, le contenu du bus n'est pas mis à jour à chaque front montant, ou à chaque front descendant, mais aux deux : fronts montant et descendant. De tels bus sont appelés des bus '''''double data rate'''''. Cela permet de transférer deux données sur le bus (une à chaque front) en un seul cycle d'horloge : le débit binaire est doublé sans toucher à la fréquence du bus.
===Les bus asynchrones===
Une minorité de bus se passent complètement de signal d'horloge : ce sont les '''bus asynchrones'''. Ils utilisent des protocoles de communication spécialisés. Les bus asynchrones permettent à deux circuits/composants de se synchroniser, l'un des deux étant un émetteur, l'autre étant un récepteur. Pour se synchroniser, l’émetteur indique au récepteur qu'il lui a envoyé une donnée, généralement grâce à un bit dédié sur le bus, souvent appelé le bit REQ. Le récepteur réceptionne alors la donnée et indique qu'il a pris en compte les données envoyées en envoyant un bit ACK. Cette synchronisation se fait grâce à des fils spécialisés du bus de commande, qui transmettent des bits particuliers.
[[File:Exemple d'écriture sur un bus asynchrone.png|centre|vignette|upright=2|Exemple d'écriture sur un bus asynchrone]]
De tels bus sont donc très adaptés pour transmettre des informations sur de longues distances (plusieurs centimètres ou plus). La raison est qu'à haute fréquence, le signal d'horloge met un certain temps pour se propager à travers le fil d'horloge, ce qui induit un léger décalage entre les composants. Plus on augmente la longueur des fils, plus ces décalages deviendront ennuyeux. Plus on augmente la fréquence, plus la période est dominée par le temps de propagation de l'horloge dans le fil. Les bus asynchrones n'ont pas ce genre de problèmes.
==La largeur du bus==
[[File:Parallel and Serial Transmission.gif|vignette|Comparaison entre bus série et parallèle.]]
La plupart des bus peuvent échanger plusieurs bits en même temps et sont appelés '''bus parallèles'''. Mais il existe des bus qui ne peuvent échanger qu'un bit à la fois : ce sont des '''bus série'''.
===Les bus série===
On pourrait croire qu'un bus série ne contient qu'un seul fil pour transmettre les données, mais il existe des contrexemples. Généralement, c'est le signe que le bus n'utilise pas un codage NRZ, mais une autre forme de codage un peu plus complexe. Par exemple, le bus USB utilise deux fils D+ et D- pour transmettre un bit. Pour faire simple, lorsque le fil D+ est à sa tension maximale, l'autre est à zéro (et réciproquement).
La transmission et la réception sur un bus série demande de faire une conversion entre les données, qui sont codées sur plusieurs bits, et le flux série à envoyer sur le bus. Cela s'effectue généralement en utilisant des registres à décalage, commandés par des circuits de contrôle.
[[File:Schema uart.png|centre|vignette|upright=2.0|Interface de conversion série-parallèle (UART).]]
===Les bus parallèles===
Passons maintenant aux bus parallèles. Pour information, si le contenu d'un bus de largeur de <math>L</math> bits est mis à jour <math>F</math> fois par secondes, alors son débit binaire est de <math>L \times F</math>. Mais contrairement à ce qu'on pourrait croire, les bus parallèles ne sont pas plus rapides que les bus série. Sur les bus synchrones, la fréquence est bien meilleure pour les bus série que pour les bus parallèles. La fréquence plus élevée l'emporte sur la largeur plus faible, ce qui surcompense le fait qu’un bus série ne peut envoyer qu'un bit à la fois. Le même problème se pose pour les bus asynchrones : le temps entre deux transmissions est plus grand sur les bus parallèles, alors qu'un bus série n'a pas ce genre de problèmes.
Il existe plusieurs raisons à cela, qui proviennent de phénomènes électriques assez subtils. Premièrement, les fils d'un bus ne sont pas identiques électriquement : leur longueur et leur résistance changent très légèrement d'un fil à l'autre. En conséquence, un bit va se propager à des vitesses différentes suivant le fil. On est obligé de se caler sur le fil le plus lent pour éviter des problèmes à la réception. En second lieu, il y a le phénomène de ''crosstalk''. Lorsque la tension à l'intérieur du fil varie (quand le fil passe de 0 à 1 ou inversement), le fil va émettre des ondes électromagnétiques qui perturbent les fils d'à côté. Il faut attendre que la perturbation électromagnétique se soit atténuée pour lire les bits, ce qui limite le nombre de changements d'état du bus par seconde.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La carte mère, chipset et BIOS
| prevText=La carte mère, chipset et BIOS
| next=Les encodages spécifiques aux bus
| nextText=Les encodages spécifiques aux bus
}}{{autocat}}
</noinclude>
156dhjrcte15bz0g6yygvc3xdx1fo7r
Fonctionnement d'un ordinateur/Les cellules mémoires
0
65778
774568
727650
2026-10-09T22:22:47Z
Mewtow
31375
/* La cellule en technologie MOS, NMOS ou PMOS, dite 4T-SRAM */
774568
wikitext
text/x-wiki
De nos jours, ces cellules mémoires sont fabriquées avec des composants électroniques et il nous faudra impérativement passer par une petite étude de ces composants pour comprendre comment fonctionnent nos mémoires. Dans les grandes lignes, les mémoires RAM et ROM actuelles sont toutes composées de '''cellules mémoires''', des circuits capables de retenir un bit. En prenant plein de ces cellules et en ajoutant quelques circuits électroniques pour gérer le tout, on obtient une mémoire. Dans ce chapitre, nous allons apprendre à créer nos propres bits de mémoire à partir de composants élémentaires : des transistors et des condensateurs.
==L'interface d'une cellule mémoire (généralités)==
Les cellules mémoires se présentent avec une interface simple, limitée à quelques broches. Et cette interface varie grandement selon la mémoire : elle n'est pas la même selon qu'on parle d'une DRAM ou d'une SRAM, avec quelques variantes selon les sous-types de DRAM et de SRAM. Là où les DRAM se limitent souvent à deux broches, les SRAM peuvent aller jusqu'à quatre. Nous reparlerons dans la suite des interfaces pour chaque type (voire sous-type) de mémoire. Pour le moment, nous allons commencer par voir le cas général. Dans les grandes lignes, on peut grouper les broches d'une cellule mémoire en plusieurs types :
* Les '''broches de données''', sur lesquelles on va lire ou écrire un bit.
* Les '''broches de commande''', sur lesquelles on envoie des ordres de lecture/écriture.
* D'autres broches, comme la broche pour le signal d'horloge ou les broches pour l’alimentation électrique et la masse.
===Les broches de données===
Concernant les broches de données, il y a plusieurs possibilités qui comprennent une, deux ou trois broches.
* Dans le cas le plus simple, la cellule mémoire n'a qu'une seule ''broche d'entrée-sortie'', sur laquelle on peut écrire ou lire un bit. On parle alors de '''cellule mémoire simple port'''.
* Les cellules mémoires plus compliquées ont une ''sortie de lecture'', sur laquelle on peut lire le bit stocké dans la cellule, et une '''entrée d'écriture''', sur laquelle on place le bit à stocker dans la cellule. Dans ce cas, on parle de '''cellule mémoire double port'''.
* Sur les '''cellules mémoires différentielles''', la cellule mémoire dispose de deux broches d’entrée-sortie, dites ''différentielles'', c'est à dire que le bit présent sur la seconde broche est l'inverse de la première. L'utilité de ces deux broches inversées n'est pas évidente, mais elle deviendra évidente dans le chapitre sur le plan mémoire.
[[File:Broches de données d'une cellule mémoire.png|centre|vignette|upright=2|Broches de données d'une cellule mémoire]]
Les cellules mémoires simple port se trouvent dans les DRAM modernes, comme nous le verrons plus bas dans la section sur les 1T-DRAM.
Les cellules mémoire double port sont présentes dans les SRAM et les DRAM anciennes. Quelques vieilles DRAM, nommées 2T et 3T-DRAM, étaient de ce type. Quand aux cellules SRAM double port, elles sont plus rares, mais on en trouve dans les mémoires SRAM multiport avec un port d'écriture séparé du port de lecture. Après tout, les bascules elles-mêmes ont un "port" d'écriture et un de lecture, ce qui se marie bien avec ce genre de mémoire multiport.
Quand aux cellules mémoires différentielles, toutes les SRAM modernes sont de ce type.
===Les broches de commande===
Pour les broches de commande, il y a deux possibilités : soit la cellule reçoit un bit Enable couplé à un bit R/W, soit elle possède deux bits qui autorisent respectivement les lectures et écriture.
* La forme la plus simple est une '''broche de sélection''' qui autorise/interdit les communications avec la cellule mémoire. Cette broche de sélection connecte ou déconnecte les autres broches du reste de la mémoire. Elle est couplée, dans le cas des mémoires RAM, à une '''broche R/W''', sur laquelle on vient placer le fameux bit R/W, qui dit s'il faut faire une lecture ou une écriture.
* Encore une fois, elle peut être scindée en une '''broche d'autorisation de lecture''' et une '''broche d'autorisation d'écriture''', sur laquelle on place le bit R/W (ou son inverse) pour autoriser/interdire les écritures.
[[File:Broches de commande d'une cellule mémoire.png|centre|vignette|upright=2|Broches de commande d'une cellule mémoire.]]
Il est possible de passer d'une interface à l'autre assez simplement, grâce à un petit circuit à ajouter à la cellule mémoire. Cela est utile pour faciliter la conception du contrôleur mémoire. Celui-ci peut en effet générer assez simplement le signa Enable, à envoyer sur la broche de sélection. Quant au bit R/W, il est fournit directement à la mémoire, via le bus de commande. L'interface avec les broches Enable et R/W est donc la plus facile à utiliser. Mais si on regarde l'intérieur de certaines cellules mémoire (celles de SRAM, notamment), on s’aperçoit que leur organisation interne se marie très bien avec la seconde interface, celle avec une broche d'autorisation de lecture et une pour autoriser les écritures. Il faut donc faire la conversion de la seconde interface vers la première.
Pour cela, on ajoute un petit circuit qui convertit les bits Enable et R/W en signaux d'autorisation de lecture/écriture. On peut établir la table de vérité de ce circuit assez simplement. Déjà, les deux bits d'autorisation ne sont à 1 que si le signal de sélection est à 1 : s'il est à 0, la cellule mémoire doit être totalement déconnectée du bus. Ensuite, la valeur de ces deux bits sont l'inverse l'une de l'autre : soit on fait une lecture, soit on fait une écriture, mais pas les deux en même temps. Pour finir, on peut utiliser la valeur de R/W pour savoir lequel des deux bit est à mettre à 1. On a donc le circuit suivant.
[[File:Circuit de gestion des signaux de commande d'une cellule de SRAM.png|centre|vignette|upright=1|Circuit de gestion des signaux de commande d'une cellule de SRAM]]
==Les cellules de SRAM==
Avant toute chose, faisons un petit point terminologique. Techniquement, les cellules mémoire d'une SRAM peuvent se fabriquer de deux manières différentes. La toute première utilise tout simplement une bascule D, la seconde utilise utilise un montage de plusieurs transistors. Le terme '''cellule de SRAM''' (''SRAM cell'') désigne uniquement la seconde possibilité. Il faut ainsi faire la distinction entre une bascule D fabriquée avec des portes logiques, et une cellule de SRAM fabriquée directement avec des transistors. La distinction est assez subtile et pas intuitive : le terme ''cellule de SRAM'' fait référence à une méthode de fabrication de la cellule mémoire, pas à la mémoire qui l'utilise. Il est possible de créer une mémoire SRAM en utilisant des bascules D ou des cellules de SRAM, les deux sont possibles. Il s'agit là d'une distinction qui ne sera pas faite pour les autres cellules mémoire.
Nous ne reviendrons pas sur les bascules D, pour ne pas faire de redite des chapitres précédents. Par contre, nous allons étudier les cellules de SRAM. Les cellules de SRAM ont bien évoluées depuis les toutes premières versions jusqu’au SRAM actuelles. Les SRAM modernes arrivent à se débrouiller avec un nombre de transistors qui se compte sur les doigts d'une main. Les variantes les plus légères se contentent de 4 transistors, les intermédiaires de 6, et les plus grosses de 8 transistors. En comparaison, les bascules D sont composées de 10 à 20 transistors.
La différence de nombre de transistors fait que les mémoires SRAM ont une meilleure densité, à savoir que l'on peut mettre plus de cellules SRAM sur une même surface. En général, moins la cellule contient de transistors, moins elle prend de place et plus la RAM aura une grande capacité mémoire. Par contre, cela se fait au détriment des performances. Ainsi, une SRAM fabriquée avec des bascules D sera très rapide, mais aura une faible capacité. Une SRAM fabriquée avec des cellules de SRAM aura une meilleure capacité, mais des performances moindres. En conséquence, les bascules D sont surtout utilisées pour les registres du processeur, alors que les cellules de SRAM sont utilisées pour les mémoires caches.
L'interface d'une cellule SRAM varie beaucoup suivant la cellule utilisée, mais nous allons parler des trois cas les plus courants, à savoir les SRAM double port, simple port et différentielles.
===Les cellules de SRAM différentielles de type 6T-SRAM et 4T-SRAM===
Les cellules SRAM différentielles n'ont pas une seule broche d'entrée-sortie, mais deux. Les deux broches sont dites différentielles, c'est à dire que le bit présent sur la seconde entrée est l'inverse du premier. Elles sont souvent notées <math>\text{BL}</math> et <math>\overline{BL}</math>. L'utilité de ces deux broches inversées n'est pas évidente, mais elle deviendra évidente dans le chapitre sur le plan mémoire.
Dans les chapitres du début du cours, nous avions vu qu'une bascule D est composée de deux inverseurs reliés l'un à l'autre de manière à former une boucle, avec des circuits annexes associés (un multiplexeur, notamment). Les cellules de SRAM utilisent elles aussi une boucle avec deux portes NON, mais ajoutent à peine quelques transistors autour, le strict minimum pour que le circuit fonctionne. Les deux broches d'entrée-sortie de la cellule sont directement connectées aux entrées des inverseurs, à travers deux transistors de contrôle, comme montré ci-dessous. Le tout ressemble au circuit précédent, sauf qu'on aurait retiré le multiplexeur.
[[File:SRAM Cell Inverter Loop.png|centre|vignette|upright=2|Cellule de SRAM.]]
Le circuit se comporte différemment entre les lectures et écritures. Lors d'une écriture, les broches servent d'entrée, et elles ont la priorité car le courant envoyé sur ces entrées est plus important que le courant qui circule dans la boucle. De plus, les transistors de contrôle sont plus gros et ont une amplification plus importante que les transistors des inverseurs. Si on veut écrire un 1 dans la cellule SRAM, la broche BL aura la priorité sur la sortie de l'inverseur et la bascule mémorisera bien un 1. Lors de l'écriture d'un 0, ce sera cette fois-ci l'entrée <math>\overline{BL}</math> qui aura un courant plus élevé que la sortie de l'autre inverseur, et la cellule mémorisera bien un 0, mais qui sera injecté par l'autre entrée. Lors d'une lecture, les broches n'ont aucun courant d'entrée, ce qui fait que les inverseurs fourniront un courant plus fort que celui présent sur la broche. Le contenu de la bascule est cette fois-ci envoyé dans la broche d'entrée-sortie.
Un problème de cette cellule est que les portes logiques fournissent peu de courant, ce qui est gênant lors d'une lecture. Mais expliquer en quoi cela est un problème ne peut pas se faire pour le moment, il vous faudra attendre le chapitre suivant. Toujours est-il que la faiblesse de la sortie des inverseurs est compensée en-dehors de la cellule SRAM, par des circuits spécialisés d'une mémoire, que nous verrons dans le chapitre suivant. Il s'agit de l'amplificateur de lecture, ainsi que des circuits de précharge, qui sont partagés entre toutes les cellules mémoires. N'en disons pas plus pour le moment.
Il existe plusieurs types de cellules différentielles de SRAM, qui se distinguent par la technologie utilisée : bipolaire, CMOS, PMOS, NMOS, etc. Chaque type a ses avantages et inconvénients : certaines fonctionnent plus vite, d'autres prennent moins de place, d'autres consomment moins de courant, etc. La différence tient dans la manière dont ont conçus les portes NON dans la cellule.
====La cellule en technologie CMOS, dite 6T-SRAM====
Les deux inverseurs peuvent être conçus en utilisant la technologie CMOS, bipolaire, NMOS ou PMOS. Dans le cas de la technologie CMOS, chaque inverseur est réalisé avec deux transistors, un PMOS et un NMOS, comme nous l'avons vu dans le chapitre sur les portes logiques. La cellule mémoire obtenue est alors une cellule à 6 transistor : 2 pour l'autorisation des lectures et écritures et 4 pour la cellule de mémorisation proprement dite (les inverseurs tête-bêche).
[[File:Cellule mémoire de SRAM - rôle de chaque transistor.png|centre|vignette|upright=2|Cellule mémoire de SRAM - rôle de chaque transistor.]]
Ce montage a divers avantages, le principal étant sa très faible consommation électrique. Mais son grand nombre de transistors fait que chaque cellule prend beaucoup de place. On ne peut donc pas l'utiliser pour construire des mémoires de grande capacité.
====La cellule en technologie NMOS/PMOS, dite 4T-SRAM====
En technologie MOS, ainsi qu'en technologie PMOS et NMOS, chaque porte logique est créée avec un transistor et une résistance. La cellule contient alors, au total, quatre transistors et deux résistances. Le circuit obtenu avec la technologie MOS est illustré ci-dessous.
[[File:SRAM Cell (4 Transistors).svg|centre|vignette|upright=2|Cellule de SRAM de type 4T-2R (à 4 transistors et 2 résistances).]]
Il est possible de remplacer la résistance par un transistor MOS câblé d'une manière précise, avec un montage dit en résistance variable. Ce montage fait que le transistor MOS se comporte comme une source de courant, équivalente au courant qui traverse la résistance.
[[File:Exemple de cellule de SRAM de technologie MOS.png|centre|vignette|upright=2|Exemple de cellule de SRAM de technologie MOS.]]
D'autres cellules retirent les résistances de charge, histoire de gagner un peu de place. La réduction de taille de la cellule mémoire est assez intéressante, mais se fait au prix d'une plus grande complexité de la cellule. L'alimentation VDD doit être fournie à l'extérieur de la cellule mémoire, qui doit être alimentée à travers les transistors d'accès. Ceux-ci sont ouverts en-dehors des lectures et écritures, une tension devant être fournie sur leur source/drain, par l'extérieur de la cellule.
[[File:Cellule de SRAM MOS alimentée par les bitlines.png|centre|vignette|upright=2|Cellule de SRAM MOS alimentée par les bitlines.]]
====La cellule en technologie bipolaire====
Les cellules de SRAM en version bipolaire sont de loin les plus rapides, mais leur consommation électrique est bien plus élevée. C'est pour cette raison que les RAM actuelles sont toutes réalisées avec une technologie MOS ou CMOS. Presque aucune mémoire n'est réalisée en technologie bipolaire à l'heure actuelle.
Dans le cas le plus simple, qui utilise le moins de composants, la commande du transistor a lieu au niveau des émetteurs des transistors, qui sont reliés ensemble.
[[File:Cellule de SRAM en technologie bipolaire.png|centre|vignette|upright=2|Cellule de SRAM en technologie bipolaire.]]
Il est possible d'améliorer le montage en ajoutant deux diodes, une en parallèle de chaque résistance. Cela permet d'augmenter le courant dispensé par la cellule mémoire lors d'une lecture ou écriture. Cela a son utilité, comme on le verra dans le prochain chapitre (pour anticiper : cela rend plus rapide la charge/décharge de la ligne de bit, sans système de précharge). Mais cela demande d'inverser les connexions dans la cellule mémoire. Le circuit obtenu est le suivant.
[[File:Cellule SRAM en techno bipolaire, avec ajout de diodes en parallèle.png|centre|vignette|upright=2|Cellule SRAM en techno bipolaire, avec ajout de diodes en parallèle.]]
===Les cellules SRAM double port===
Il existe des cellules SRAM double port, avec deux entrées d'écriture et une sortie spécifique pour la lecture. Elles sont similaires aux cellules précédentes, sauf qu'on a rajouté deux transistors pour la lecture. Cela fait en tout 8 transistor, d'om le nom de 8T-SRAM donné à ce type de cellules mémoires. Le circuit précédent à six transistors est utilisé tel quel pour l'écriture. Si on utilise deux transistors pour le port de lecture, c'est pour une raison assez simple. Le premier transistor sert à connecter la sortie de l'inverseur à la ligne de bit (le fil sur lequel on récupère le bit), quand on sélectionne la cellule mémoire. Le second transistor, quant à lui est facultatif. Il est utilisé pour amplifier le signal de sortie de l'inverseur, pour l'envoyer sur la ligne de bit, pour des raisons que nous verrons au prochain chapitre.
[[File:8T SRAM.png|centre|vignette|upright=2|8T SRAM]]
==Les cellules de DRAM==
Comme pour les SRAM, les DRAM sont composées d'un circuit qui mémorise un bit, entouré par des transistors pour autoriser les lectures et écritures. La différence avec les SRAM tient dans le circuit utilisé pour mémoriser un bit. Contrairement aux mémoires SRAM, les mémoires DRAM ne sont pas fabriquées avec des portes logiques. À la place, elles utilisent un composant électronique qui sert de "réservoir" à électrons. Un réservoir remplit code un 1, alors qu'un réservoir vide code un 0.
La nature du réservoir dépend cependant de la version de la cellule de mémoire DRAM utilisée. Car oui, il existe plusieurs types de cellules de DRAM, qui utilisent des composants réservoir différents. Étudier les versions plus récentes, actuellement utilisées dans les mémoires DRAM modernes, est bien plus facile que de comprendre les versions plus anciennes. Aussi, nous allons commencer par le cas le plus simple : les cellules de DRAM dites 1T-DRAM. Nous verrons ensuite les cellules de type 3T-DRAM, plus complexes et plus anciennes.
===Les DRAM à base de condensateurs : 1T-DRAM===
[[File:Parallel plate capacitor.svg|vignette|Condensateur.]]
Les DRAM actuelles n'utilisent qu'un seul transistor, associé à un autre composant électronique nommé '''condensateur'''. Ce condensateur est un réservoir à électrons : on peut le remplir d’électrons ou le vider en mettant une tension sur ses entrées. Il stocke un 1 s'il est rempli, un 0 s'il est vide. C'est donc lui qui sert de circuit de mémorisation. On peut naturellement remplir ou vider un condensateur (on dit qu'on le charge ou qu'on le décharge), ce qui permet d'écrire un bit à l'intérieur.
À côté du condensateur, on ajoute un transistor qui va autoriser l'écriture ou la lecture dans notre condensateur. Tant que le transistor se comporte comme un interrupteur ouvert, le condensateur est isolé du reste du circuit : pas d'écriture ou de lecture possible. Il faut l'ouvrir pour lire ou écrire dans le condensateur.
La cellule de DRAM a donc deux broches : une broche de données connectée indirectement au condensateur à travers le transistor MOS, une broche de commande connectée à la grille du transistor MOS. La broche de commande est connectée à un fil appelé la ''word line'' ou '''ligne de mot''', la broche de données est connectée à un fil appelé la ''bitline'' ou '''ligne de bit'''. Retenez bien ces termes, nous les utiliserons beaucoup dans ce qui suit.
[[File:1T-DRAM.png|centre|vignette|upright=2|1T-DRAM.]]
Le condensateur est connecté au transistor MOS d'un côté, à la masse (le 0 volts) de l'autre. Du moins, c'était le cas avant, les DRAM actuelles ne connectent pas le condensateur à la masse, mais à une tension égale à la moitié de la tension d'alimentation, pour des raisons qu'on expliquera plus tard. Le condensateur stocke alors un 1 quand la tension est positive, un 0 quand elle est négative.
====Le rafraichissement mémoire====
L'intérieur d'un condensateur n'est pas très compliqué : il est formé de deux couches de métal conducteur, séparées par un isolant électrique. Les deux plaques de conducteur sont appelées les armatures du condensateur. C'est sur celles-ci que les charges électriques s'accumulent lors de la charge/décharge d'un condensateur. L'isolant empêche la fuite des charges d'une armature à l'autre, ce qui permet au condensateur de fonctionner comme un réservoir, et non comme un simple fil.
Mais sur les DRAM actuelles, les condensateurs sont tellement miniaturisés qu'ils en deviennent de vraies passoires. Il possède toujours quelques défauts et des imperfections qui font que l'isolant n'est jamais totalement étanche : des électrons passent de temps en temps d'une armature à l'autre et quittent le condensateur. En clair, le bit contenu dans la cellule de mémoire DRAM s'efface. Ce qui explique qu'on doive rafraîchir régulièrement les mémoires DRAM de ce type.
====La lecture et l'écriture d'une cellule de 1T-DRAM====
Le circuit précédent a deux broches : une broches de données et une broche de contrôle. La broche de données est celle sur laquelle on récupère un bit lors d'une lecture, mais aussi sur laquelle on envoie le bit lors d'une écriture. Elle est connectée à la ligne de bit. La broche de commande sert à connecter le condensateur au bus de données lors d'une lecture ou écriture, elle est connectée à la ligne de mot.
Lors d'une écriture, la broche de données est mise à 0 ou 1. Si elle est mise à 0, le condensateur va se vider intégralement dans le fil et restera à zéro une fois vidé. Si l'entrée-sortie est mise à 1, le condensateur se remplit, s'il n'est pas déjà remplit. Au final, un 1 sera stocké dedans.
Lors d'une lecture, le condensateur est connecté sur la broche de données et se vide entièrement dedans. Il faut le récrire après chaque lecture sous peine de perdre le contenu de la cellule de DRAM. Il s'agit d'une forme de rafraichissement mémoire, dans le sens où on doit rafraichir le contenu de la cellule mémoire après chaque lecture.
Pire : le condensateur se vide sur le bus, mais cela ne suffit pas à créer une tension de plus de quelques millivolts dans celui-ci. Pas de quoi envoyer un 1 sur le bus ! Mais il y a une solution : amplifier la tension de quelques millivolts induite par la vidange du condensateur sur le bus, avec un circuit adapté. Les circuits de rafraichissement sur lecture et celui d'amplification sont souvent fusionnés en un seul circuit qui fait les deux, comme on le verra plus tard.
====La conception du condensateur====
Une DRAM peut stocker plus de bits pour la même surface qu'une SRAM : un transistor couplé à un condensateur prend moins de place que 6 transistors. Un autre avantage est que les deux peuvent s'empiler l'un au-dessus de l'autre ce qu'on ne peut pas faire avec des transistors CMOS, ce qui améliore encore la densité des DRAM par rapport aux autres mémoires.
Au tout début, le transistor et le condensateur étaient placés l'un à côté de l'autre, sur le même plan. Le condensateur était alors appelé un '''condensateur planaire'''. Mais depuis les années 80, le condensateur et le transistor sont placés l'un au-dessus de l'autre, ou l'un en-dessous de l'autre. Si le condensateur est placé au-dessus du transistor, on parle de '''condensateur empilés''' (''stacked capacitor''). Mais si le condensateur est placé en-dessous du transistor, on parle de '''condensateur enterré''' (''trench capacitor'').
La taille d'une cellule de DRAM est souvent mesurée en utilisant une mesure appelée le <math>F^2</math>. F est approximativement égal à la finesse de gravure, mais prenez garde à ne pas faire de comparaison avec la finesse de gravure des transistors CMOS. L'aire <math>F^2</math> est l'unité de base pour comparer la taille des cellules DRAM, qui est un multiple de <math>F^2</math>, et vaut donc : <math>n F^2</math>. Il existe trois types de DRAM en fonction de la place prise par la cellule de DRAM : les DRAM <math>8F^2</math> où la taille de la cellule mémoire est de 8 fois celle d'une unité de base, les DRAM <math>6F^2</math> où c'est seulement 6 fois, et les DRAM <math>4F^2</math> où c'est seulement 4 fois.
L'arrangement le plus simple est l''''arrangement 4F2'''. Avec lui, les cellules de DRAM sont carrées. Il ne s'agit pas de l'arrangement le plus utilisé, car les cellules mémoires ne sont pas assez denses pour le moment. Le tout est illustré ci-dessous. La cellule de DRAM est le carré orange, les lignes de bits sont en vert et les lignes de mot en jaune. On voit que le transistor est situé au-dessus, que le transistor est composé des structures 6, 7 et 8 (source, grille et drain), avec une connexion directe à la ligne de mot sur la grille en 7. La ligne de bit est située tout en-dessous, elle est connectée au drain (le numéro 8).
[[File:DRAM Cell Structure (4F2).PNG|centre|vignette|upright=1.5|1. Ligne de mots 2. Ligne de bits 3. Condensateur 4. Taille d'une cellule 5. Condensateur 6. Source 7. Canal 8. Drain 9. Film isolant de grille ]]
L'arrangement actuel est cependant différent, car les cellules de DRAM ne sont pas carrées, mais rectangulaires. Dans l'''arrangement 8F2''', les cellules de DRAM sont deux fois plus longues que larges. Le tout donne ce qui est illustré dans le schéma suivant, où la cellule de DRAM est le rectangle orange, les lignes de bits sont en vert et les lignes de mot en jaune. La forme rectangulaire de la cellule de DRAM impose de placer les lignes de bit et de mot d'une manière bien précise. On s'attendrait que à ce que les lignes de mot soient deux fois plus éloignées que les lignes de bits (ou l'inverse), mais ce n'est pas le cas ! La distance est la même entre toutes les cellules. Pour cela, les cellules de DRAM ne sont pas alignées, mais décalées d'une ligne à l'autre.
[[File:DRAM Cell Structure (8F2).PNG|centre|vignette|upright=1.5|1. Ligne de mots 2. Ligne de bits 3. Condensateur 4. Taille d'une cellule ]]
L'''arrangement 6F2''' utilise des cellules de mémoire 1.5 fois plus longues que large. Il est utilisé depuis les années 2010.
===Les DRAM à base de transistors : 3T-DRAM et 2D-DRAM===
Les premières mémoires DRAM fabriquées commercialement n'utilisaient pas un condensateur comme réservoir à électrons, mais un transistor. Pour rappel, tout transistor MOS a un pseudo-condensateur caché entre la grille et la liaison source-drain. Pour comprendre ce qui se passe dans ce transistor de mémorisation, il savoir ce qu'il y a dans un transistor CMOS. À l'intérieur, on trouve une plaque en métal appelée l'armature, un bout de semi-conducteur entre la source et le drain, et un morceau d'isolant entre les deux. L'ensemble forme donc un condensateur, certes imparfait, qui porte le nom de capacité parasite du transistor. Suivant la tension qu'on envoie sur la grille, l'armature va se remplir d’électrons ou se vider, ce qui permet de stocker un bit : une grille pleine compte pour un 1, une grille vide compte pour un 0.
[[File:Transistor CMOS - 1.png|centre|vignette|upright=2|Anatomie d'un transistor CMOS]]
L'armature n'est pas parfaite et elle se vide régulièrement, d'où le fait que la mémoire obtenue soit une DRAM. Comme avec les autres DRAM, le bit stocké dans la capacité parasite doit être rafraîchit régulièrement. Avec cette organisation, lire un bit ne le détruit pas : on peut relire plusieurs fois un bit sans que celui-ci ne soit effacé. C'est une qualité que les DRAM modernes n'ont pas.
====3T-DRAM====
Les premières DRAM de ce type utilisaient 3 transistors, d'où leur nom de '''3T-DRAM'''. Le bit est mémorisé dans celui du milieu, indiqué en bleu sur le schéma suivant, les deux autres transistors servant pour les lectures et écritures.
[[File:3T-DRAM.png|centre|vignette|upright=1|3T-DRAM.]]
Cette organisation donne donc, dans le cas le plus simple, une cellule avec quatre broches : deux broches de commandes, une pour la lecture et une pour l'écriture, ainsi qu'une entrée d'écriture et une sortie de lecture. Mais il est possible de fusionner certaines broches à une seule. Par exemple, on peut fusionner la broche de lecture avec celle d'écriture. De toute façon, les broches de commande diront si c'est une lecture ou une écriture qui doit être faite. De même, il est possible de fusionner les signaux de lecture et d'écriture, afin de faciliter le rafraîchissement de la mémoire. Avec une telle cellule, et en utilisant un contrôleur mémoire spécialement conçu, toute lecture réécrit automatiquement la cellule avec son contenu. Pour résumer, quatre cellules différentes sont possibles, selon qu'on fusionne ou non les broches de données et/ou les broches de commande. Ces quatre possibilités sont illustrées ci-dessous.
[[File:Organisations possibles d'une cellule de 3T-DRAM.png|centre|vignette|upright=2|Organisations possibles d'une cellule de 3T-DRAM]]
====2T-DRAM====
Une amélioration des 3T-DRAM permet d'éliminer un transistor. Plus précisément, l'idée est de fusionner le transistor qui stocke le bit et celui qui connecte la cellule à la bitline de lecture. Le tout donne une DRAM fabriquée avec seulement deux transistors, d'où leur nom de '''2T-DRAM'''. La cellule 2T-DRAM est illustrée ci-dessous.
[[File:DRAM 2T-DRAM.png|centre|vignette|upright=2|Cellule d'une 2T-DRAM.]]
==Les cellules des mémoires EPROM, EEPROM et Flash==
Dans le chapitre sur les généralités des mémoires, nous avons vu les différents types de ROM : ROM proprement dite (''mask'' ROM), PROM, EPROM, EEPROM, et mémoire Flash. Ces différents types ne fonctionnent évidement pas de la même manière, non seulement au niveau du contrôleur mémoire, mais aussi des cellules mémoires. Les cellules mémoires des ''mask'' ROM et PROM sont un peu à part, dans le sens où elles n'ont pas vraiment de cellule mémoire proprement dit. C'est ce qui fait que le fonctionnement des mémoires PROM et ROM seront vues plus tard, dans un chapitre dédié. Dans ce qui va suivre, nous n'allons voir que les mémoires de type EPROM et leurs dérivés (EEPROM, Flash). Toutes fonctionnent avec le même type de cellule mémoire, les différences étant assez mineures.
===Les transistors à grille flottante===
Les mémoires EPROM, EEPROM et Flash sont fabriquées avec des '''transistors à grille flottante''' (un par cellule mémoire). On peut les voir comme une sorte de mélange entre transistor et condensateur. Un transistor MOS normal est composé d'une grille métallique et d'une tranche de semi-conducteur, séparés par un isolant, ce qui en fait un mini-condensateur. Un transistor à grille flottante, quant à lui, possède deux armatures et deux couches d'isolant. La seconde armature est celle qui sert de condensateur, celle qui stocke un bit : il suffit de la remplir d’électrons pour stocker un 1, et la vider pour stocker un 0. La première armature sert de grille de contrôle, de signal qui autorise ou interdit le remplissage de la seconde armature.
[[File:Transistor à grille flottante.png|centre|vignette|upright=2|Transistor à grille flottante.]]
Pour effacer une EPROM, on doit soumettre la mémoire à des ultra-violets : ceux-ci vont donner suffisamment d'énergie aux électrons coincés dans l'armature pour qu'ils puissent s'échapper. Pour les EEPROM et les mémoires Flash, ce remplissage ou vidage se fait en faisant passer des électrons entre la grille et le drain, et en plaçant une tension sur la grille : les électrons passeront alors dans l'armature à travers l'isolant.
===Les différents types de cellules EEPROM/Flash : SLC/MLC/TLC/QLC===
Sur la plupart des EEPROM, un transistor à grille flottante sert à mémoriser un bit. La tension contenue dans la seconde armature est alors divisée en deux intervalles : un pour le zéro, et un autre pour le un. De telles mémoires sont appelées des mémoires SLC (''Single Level Cell''). Mais d'autres EEPROM utilisent plus de deux intervalles, ce qui permet de stocker plusieurs bits par transistor : les mémoires MLC (''Multi Level Cell'') stockent 2 bits par cellules, les mémoires TLC (''Triple Level Cell'') stockent 3 bits, les mémoires QLC (''Quad Level Cell'') en stockent 4, etc.
[[File:Cell types SLC-PLC in comparison 20211102.svg|centre|vignette|upright=2|Types de cellules mémoires d'EEPROM/Flash.]]
Évidemment, utiliser un transistor pour stocker plusieurs bits aide beaucoup les mémoires non-SLC à obtenir une grande capacité, mais cela se fait au détriment des performances et de la durabilité de la cellule mémoire. Typiquement, plus une cellule de mémoire FLASH contient de bits, moins elle est performante en lecture et écriture, et plus elle tolère un faible nombre d'écritures/lectures avant de rendre l'âme. Pour les performances, cela s'explique par le fait que la lecture et l'écriture doivent être plus précises sur les mémoires MLC/TLC/QLC, elles doivent distinguer des niveaux de tensions assez proches, là où l'écart entre un 0 et un 1 est assez important sur les mémoires SLC.
===La lecture et l'écriture des cellules des mémoires EEPROM===
Sur les EEPROM, la reprogrammation et l'effacement de ces cellules demande de placer les bonnes tensions sur la grille, le drain et la source. Le procédé exact est en soit très simple, mais comprendre ce qui se passe exactement est une autre paire de manches. Les phénomènes qui se produisent dans le transistor à grille flottante lors d'une écriture ou d'un effacement sont très compliqués et font intervenir de sombres histoires de mécanique quantique. C’est la raison pour laquelle nous ne pouvons pas vraiment en dire plus.
{|
|[[File:Flash-Programming.svg|lang=fr|vignette|upright=1.0|Mise à 1 de la cellule mémoire (reprogrammation).]]
|[[File:Flash erase.svg|lang=fr|vignette|upright=1.0|Mise à zéro de la cellule mémoire (effacement).]]
|}
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le bus mémoire
| prevText=Le bus mémoire
| next=Le plan mémoire
| nextText=Le plan mémoire
}}
</noinclude>
gpuu50nw6g6m2k10dcu1g1dgpe8wpol
Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions
0
65843
774530
761274
2026-10-09T18:15:27Z
Mewtow
31375
/* L'émission et les micro-opérations mémoire */
774530
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir les processeurs à émission dans l'ordre et allons parler longuement des '''dépendances de données'''. Pour rappel, deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) au même registre ou à la même adresse mémoire. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. C'est le cas le plus évident, mais on a aussi des cas moins intuitifs. Voyons lesquels et comment ceux-ci sont gérés par le pipeline.
==Les dépendances de données==
Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c'est à dire au même registre ou la même adresse mémoire. En tout, cela fait quatre possibilités :
* '''Read after read''' : Deux instructions lisent la même donnée, mais pas en même temps.
* '''Read after write''' : Une instruction a pour opérande le résultat d'une autre : elle lit le résultat écrit par la première.
* '''Write after write''' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, mais pas en même temps.
* '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Pour les dépendances ''Read after read'', on peut mettre les deux instructions dans n'importe quel ordre, cela ne pose aucun problème. Ce ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, ce n'est pas le cas avec les trois autres types de dépendances, qui imposent que la première instruction s'exécute avant la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* '''Read after write''' : La première instruction doit écrire son résultat avant que la seconde le lise.
* '''Write after write''' : La première instruction doit écrire son résultat avant la seconde.
* '''Write after read''' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
: Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW.
===Les dépendances de registre et mémoire===
Une autre distinction sépare les dépendances de registre et les dépendances d'adresse. Une dépendance de donnée survient quand deux instructions lisent/écrivent le même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions. Dans la suite du cours, nous allons supposer une architecture LOAD-STORE dans la plupart des cas. Une telle architecture a une séparation stricte entre accès mémoire et les autres instructions. Les accès mémoire regroupent les instructions LOAD et STORE, qui se distinguent des autres instructions. Pour simplifier, les autres instructions seront appelées des ''opérations'', ou encore des instructions de calcul.
Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Les '''dépendances d'adresse''' surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Là encore, on a le cas des dépendances RAW, WAW et WAR.
Les dépendances d'adresse se manifestent sur tous les pipelines, même ceux qui ne sont pas multicycles. La raison est que les écritures se font à la toute fin du pipeline, pour gérer les exceptions précises, alors que les lectures se font quelques étages avant. Il peut alors y avoir quelques problèmes, lorsque plusieurs instructions mémoire se suivent. Imaginez par exemple qu'une lecture s'exécute avant qu'une écriture atteigne la fin du pipeline. Si les deux instructions accèdent à la même donnée, on a une dépendance d'adresse de type RAW. Le cas sera étudié dans le chapitre sur le contournement.
Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites. Et autant les registres lus/écrit sont connus dès que l'instruction est décodée, autant les adresses sont souvent calculées à l'exécution ou mémorisées dans des registres. Les deux types de dépendances sont donc le fait de circuits séparés, qui sont dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité d'accès mémoire et notamment de la file d'écriture, les autres dépendances sont gérées pendant/après l'étape de décodage d'instruction. Aussi, il est important de séparer les deux le plus possible.
===Les dépendances de registre===
Dans ce qui va suivre, nous allons nous limiter aux dépendances liées aux registres, et mettre de côté les dépendances d'adresse. Intuitivement, on se dit que de les dépendances de registres concernent les instructions arithmétiques, mais les instructions d'accès mémoire peuvent avoir des dépendances de registre avec d'autres instructions. L'instruction STORE lit la donnée à écrire et son adresse dans les registres, elle a donc potentiellement des dépendances de registre, à cause des registre opérande. Si une autre instruction lit/écrit ces registres opérandes, une dépendance de registre survient. L'instruction LOAD est dans un cas similaire, : elle récupère l'adresse à lire dans un registre opérande (la plupart du temps), et écrit la donnée lue dans un autre registre de destination. Ce qui fait deux sources de dépendances de registre : le registre opérande, le registre de destination. Si une instruction de calcul ou une autre instruction mémoire lit/écrit dans ces registres, une dépendance de donnée a lieu.
Dans ce chapitre, nous allons nous préoccuper des trois dépendances de registres suivantes :
* '''''Read after write''''' : Une instruction lit un registre dans laquelle l'instruction précédente a écrit.
* '''''Write after read''''' : Une instruction lit un registre pour récupérer une opérande, mais ce registre est ensuite écrasé par une instruction ultérieure.
* '''''Write after write''''' : Deux instructions écrivent dans le même registre, mais à des instants différents.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW.
Les dépendances RAW et WAW, imposent que la première instruction ait écrit son résultat avant que la seconde lise ses opérandes. Si on ne fait pas ça, la seconde instruction lit des opérandes invalides ou voit son écriture écrasée par une écriture précédente. Pour éviter cela, la seconde instruction doit être démarrée quand la première s'est totalement terminée. Insérer des bulles de pipeline est donc une nécessité. Mais diverses optimisations permettent de mitiger l'impact des dépendances de données. Par exemple, dans le prochain chapitre, nous parlerons de la technique du contournement, qui permet de supprimer les bulles de pipeline pour les dépendances RAW. Les dépendances WAW et WAR peuvent aussi être totalement supprimées par des techniques de renommages de registres, qui auront leur chapitre dédié.
===Les dépendances présentes dépendent du pipeline===
Le support des exceptions précises supprime pas mal de dépendances de données. Le fait que les écritures mémoire garantit que les écritures se fassent dans l'ordre du programme, il n'y a pas de situations où une écriture soit exécutée avant une écriture antérieure. Et cela suffit à faire disparaitre les dépendances WAW. De même, les dépendances d'adresse WAR sont impossibles avec des exceptions précises, car les écritures se font à la fin du pipeline, après les lectures.
Même sans exceptions précises, les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. En pratique, les dépendances WAR n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW, quant à elles, n'apparaissent que sur les pipelines dynamiques, ceux qui supportent des instructions multicycles, ou si les écritures prennent plusieurs étages.
{|class="wikitable"
|-
! Dépendance
! Pipeline
! colspan="2" | Solutions
|-
! RAW
| Tous les pipelines
| rowspan="3" | Bulles de pipeline
| Contournement
|-
! WAW
| Pipelines dynamiques
| rowspan="2" | Renommage de registres
|-
! WAR
| Pipelines dynamiques avec exécution dans le désordre
|}
Les dépendances WAR et WAW sont souvent appelées des dépendances de nom (''naming dependency'') et sont opposées aux dépendances vraies (''true dependency''), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Et on peut les faire disparaitre avec des techniques dites de renommage de registre, qui feront l'objet d'un chapitre ultérieur.
==L'émission d'une instruction et les bulles de pipeline==
Les dépendances imposent qu'une première instruction soit totalement terminée avant qu'on démarre la seconde, ou du moins qu'elle ait progressé suffisamment dans le pipeline. Pour cela, il suffit de retarder l'exécution de la seconde instruction tant que les conditions nécessaires pour son exécution ne sont pas réunies. Par exemple, prenons le cas où une instruction a pour opérande le résultat d'une autre : la seconde doit attendre que le résultat de la première soit calculé avant de démarrer, il se peut qu'elle soit retardée durant quelques cycles pour cela.
Les processeurs détectent les dépendances de registre dans un circuit spécialisé, appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée. Si une instruction a une dépendance bloquante, elle est bloquée dans l'étage d'émission en attendant de pouvoir s'exécuter. Durant ce temps d'attente, on insère des vides après l'unité d'émission : certains étages seront inoccupés et n'auront rien à faire. Ces vides sont appelés des calages (''stall''), ou '''bulles de pipeline''' (''pipeline bubble''). Les étages qui sont situés avant l'atage d'émission sont eux gelés, la progression des instructions dedans est stoppée.
[[File:Pipeline bubble.png|centre|vignette|upright=2|Pipeline bubble]]
Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données.
Dans la suite du cours, nous verrons de nombreux cas dans lesquels insérer des bulles de pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, les bulles de pipeline sont une source de perte de performance : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire l'utilisation de ces bulles de pipeline le plus possible. D'ailleurs, nous passerons les prochains chapitres à parler de ces optimisations.
===La détection des dépendances de données===
Le processeur doit détecter les dépendances de données entre instructions. Les dépendances de registres se détectent en comparant entre eux les registres utilisés par les instructions. Les registres étant connus dès la sortie de l'étape de décodage, la détection des dépendances de données se fait de manière précoce, avant même l'exécution des instructions. Une sacré différence avec les dépendances d'adresse, qui demandent que les adresses soient calculées et lues dans les registres avant qu'on puisse détecter les dépendances.
La détection des dépendances de registre se fait dans l'étage d'émission, situé juste après l'étage de décodage d'instruction. L'unité d'émission reçoit une instruction décodée et vérifie ses dépendances avec les instructions dans le pipeline. À chaque cycle d'horloge, l'unité d'émission analyse une instruction décodée appelée '''instruction candidate'''. Elle détecte si cette instruction candidate a une dépendance avec une instruction déjà dans le pipeline. Les instructions dans le pipeline n'ont pas encore enregistrées leurs résultats dans les registres, soit parce qu'elles sont en cours d'exécution, qu'elles lisent leurs opérandes, peut importe. Elles seront appelées '''instructions "en vol"'''.
Pour cela, l'unité d'émission compare les registres utilisés par l'instruction candidate et les instructions dans le pipeline. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Les quatre situations possibles sont les suivantes :
* Un des registres opérande est à lire (opérande) par une instruction dans le pipeline : pas de dépendance.
* Un des registres opérande est écrit (destination) par une instruction dans le pipeline : dépendance RAW.
* Un des registres de destination est à lire (opérande) par une instruction dans le pipeline : potentielle dépendance WAR, mais seulement sur des pipelines pathologiques jamais utilisés.
* Un des registres de destination est écrit (destination) par une instruction dans le pipeline : dépendance WAW.
Notez que pour les lectures, on part du principe que les instructions en vol n'ont pas encore lu leurs registres opérandes. Dès qu'elles ont lu les opérandes en question, les dépendances liées disparaissent. Pareil pour les écritures, sauf que celles-ci ont lieu à la toute fin du pipeline. Les écritures en attente ont donc lieu quand l'instruction termine.
{|class="wikitable"
|-
! rowspan="2" colspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! rowspan="2" | Instructions en vol
! Registres pas encore lus (opérandes)
| Pas de dépendances (dépendance RAR inoffensive)
| Dépendance WAR potentielle, pas en pratique
|-
! Registre pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
L'algorithme est donc le suivant : une lecture est autorisée en absence d'écriture (dans le même registre), une écriture est autorisée en absence de lecture (dans le même registre). Cependant, on peut grandement simplifier l'algorithme et ne pas tenir compte des dépendances WAR, qui n'existent pas sur la plupart des pipelines dynamiques sans exécution dans le désordre. Le tableau se simplifie en retirant les dépendances inutiles, seule la dernière ligne nous intéresse au final.
{|class="wikitable"
|-
! rowspan="2" colspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! Instructions en vol
! Registre pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
===L'exécution dans l'ordre et dans le désordre des instructions===
Nous venons de voir que les dépendances de données posent des problèmes qu'un pipeline de longueur variable doit résoudre. La solution la plus simple retarde les instructions candidates problématiques de quelques cycles d'horloges. Si une instruction a une dépendance problématique avec une autre, il suffit simplement de l’exécuter avec un retard de cycles. Pour cela, lorsqu'une instruction multicycle est démarrée, les instructions problématiques sont bloquées à l'étage de décodage en attendant le bon moment. Pour cela, on utilise des '''bulles de pipeline''' (''pipeline bubble''), pour retarder l'instruction fautive dans l'unité de décodage.
[[File:Pipeline bubble.png|centre|vignette|upright=2|Bulle de pipeline.]]
Notons que cette technique exécute des instructions indépendantes dans des unités de calcul séparées. Dans l'exemple précédent avec une instruction multicycle suivie par une instruction normale, le processeur détecte les situations où ce n'est pas grave si la seconde instruction finisse avant la première : on peut alors lancer la seconde instruction immédiatement sans attendre. Il y a donc un gain en performance. Un point important est que cette technique émet les instructions dans l'ordre, mais qu'elles peuvent s'exécuter dans le désordre et terminer dans le désordre. En clair, les instructions sont injectées dans les unités de calcul dans l'ordre, mais en sortent dans le désordre.
Pour éviter les bulles de pipeline, les processeurs modernes incorporent des optimisations assez intéressantes. Nous verrons la technique du contournement dans le chapitre suivant. Elle permet de réduire l'usage de bulles de pipeline en cas de dépendance ''Read after write''. Une solution complémentaire est ce qu'on appelle l''''exécution dans le désordre''', une optimisation commune sur les processeurs modernes qui sera vue en détail dans plusieurs chapitres dans la suite du cours.
L'idée est que si une instruction problématique est bloquée dans l'unité de décodage/émission, elle est juste mise en attente, elle ne bloque pas le pipeline. Le processeur regarde les instructions suivantes et vérifie si certaines d'entre elles peuvent s'exécuter. Les instructions mises en attente sont exécutées dès que possible, elles ont la priorité. La gestion de la mise en attente des instructions problématiques est assez complexe et implique de les stocker dans des mémoires spécifiques. Là encore, il faut que les instructions exécutées en parallèle ne manipulent pas les mêmes données, ni les mêmes registres, ni les mêmes adresses mémoires.
La toute première implémentation de l'exécution dans le désordre était sur l'ordinateur CDC 6600, suivi par son successeur le CDC 7600. Il intégrait un circuit appelé le '''''scoreboard''''', qui gérait l'exécution dans le désordre. Décrire le circuit en question est assez compliqué, aussi je le laisse de côté pour le moment. Sachez le terme ''scoreboard'' est resté et a été élargit à tout circuit d'émission, qu'il soit dans le désordre ou dans l'ordre.
==L'étage d'émission : la génération de bulles de pipeline==
Maintenant, voyons comment est implémenté l'étage d'émission (''issue''), qui détecte les situations problématiques et rajoute des bulles de pipeline si besoin. Il suit immédiatement l'étage de décodage, et est situé avant le chemin de données. Le rôle de l'unité d'émission est de générer les bulles de pipeline. Si elle reçoit une instruction problématique, elle la bloque dans l'unité d'émission et génère des bulles de pipeline, tant que la dépendance n'est pas réglée. Mais si l'instruction peut s'exécuter, elle l'envoie dans la suite du pipeline. On dit qu'elle émet l'instruction.
L''''émission''' est donc l'envoi d'une micro-opération dans le pipeline, si les conditions sont réunies. Un point important de l'émission est que le processeur émet une instruction par cycle, en absence de dépendance de données, mais aussi en absence de dépendances structurelles, les deux étant détectées par l'unité d'émission.
===La gestion des dépendances de données===
Pour rappel, l'unité d'émission détecte les dépendances entre une '''instruction candidate''' et les instructions déjà dans le pipeline, aussi appelées '''instructions "en vol"'''. L'unité d'émission détecte les dépendances entre l'instruction candidate et chaque instruction en vol. Pour rappel, les dépendances de données se détectent en comparant les instructions de deux opérandes. Vu que les dépendances WAR sont très rares, et n'existent pas sur les pipelines normaux, on ne tient compte que des dépendances RAW et WAW. Les détecter demande de comparer les registres de l'instruction candidate, avec les registres de destination des instructions en vol.
{|class="wikitable"
|-
! rowspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! Instructions en vol
Registres pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
L'unité d'émission doit connaitre les registres de destination des instructions en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut sortir une bulle de pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons.
[[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]]
Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être écrit par une autre instruction. Les registres qui ne sont pas réservés sont dit libres ou disponibles. Si une instruction candidate veut lire ce registre, c'est signe qu'il y a une dépendance RAW : elle veut lire un registre réservé, qui est destiné à être écrit par une instruction en vol. Si elle veut écrire, c'est signe qu'elle veut écrire dans un registre où une autre instruction veut écrire : c'est une dépendance WAW. Dans les deux cas, l'instruction n'est pas émise.
Le registre encode les registres réservés en représentation ''one-hot'' modifiée. Chaque bit est associé à un registre. Le bit numéro 0 est associé au registre numéro 0, le bit numéro 1 au registre numéro 1, etc. Un registre est réservé en mettant le bit associé à 1.
{|class="wikitable"
|+ Registre de réservation
|-
! Registre 7 !! Registre 6 !! Registre 5 !! Registre 4 !! Registre 3 !! Registre 2 !! Registre 1 !! Registre 0
|-
| 0 || 0 || 1 || 0 || 0 || 1 || 0 || 0
|}
Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''.
Avant d'émettre une instruction, l'unité d'émission extrait les registres opérande/destination et les convertis dans la même représentation que le registre de réservation, à savoir en représentation ''one-hot''. Le circuit de traduction binaire vers ''one-hot'' est, pour rappel, un simple décodeur. Puis, on fait un OU logique entre les sorties des décodeurs. Le résultat est un masque qui indique quels registres sont lus par l'instruction, encodé en représentation ''one-hot''. Appelons-le le '''masque d'opérandes'''. Le masque est alors comparé au registre de réservation pour vérifier si l'instruction peut être émise.
[[File:Unité d'émission simple, dans l'ordre.png|centre|vignette|upright=2|Unité d'émission simple, dans l'ordre]]
Si l'émission est autorisée, le registre de réservation est là aussi mis à jour. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. Le registre de réservationest aussi mis à jour dès qu'une instruction enregistre son résultat dans les registres, ou alors dès qu'il est disponible pour le contournement. Le bit associé au registre repasse alors à 0 (ou à 1). Sans contournement, la mise à jour se fait alors à la toute fin de l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul.
Vous aurez peut-être remarqué, mais une différence est que les pipelines fixes retardent les écritures dans le banc de registre avec des registres d'échelonnage, les pipelines dynamique retardent l'émission des instructions. Les deux méthodes ont sensiblement le même résultat.
Le '''registre de consultation''' permet de désigner les registres qui vont être lus par les instructions en vol. Je précise bien : les registres qui n'ont pas encore été lus. Les registres en question sont dit consulté, les autres sont dit non-consultés. Il est interdit d'écrire dans un registre consulté, c'est signe d'une dépendance WAR. Il faut noter que beaucoup de pipeline s'en passent car ils n'ont pas de dépendances WAR. Le registre de consultation est mis à jour dès que l'opérande est lue. Et cela arrive assez tôt dans le pipeline, au bout de quelques cycles grand maximum. Sur beaucoup de pipelines, il n'est pas présent dans il est impossible qu'une écriture ait lieu avant qu'une lecture ait démarrée. Mais sur les pipelines où de telles dépendances sont possibles, il faut en tenir compte.
Il existe quelques rares processeurs qui fusionnent le registre de réservation et de consultation en un seul. Le registre contient autant de bits que de registres, chaque bit indique si le registre associé est pas encore lu/écrit. De tels implémentations détectent alors toutes les dépendances, même les dépendances ''Read After Read''. Elles tendent à être très conservatrices. Un exemple est le co-processeur vectoriel VFP9-S d'ARM. Leur implémentation est simple, mais il peut y avoir un cout en performance.
===La gestion de la disponibilité des ALUs===
L'étage d'émission détecte aussi les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission émet des bulles de pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles.
Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit insérer des bulles de pipeline.
Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''.
{|class="wikitable"
|+ Vecteur de disponibilité des ALUs
|-
! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY
|-
| 0 || 0 || 1 || 0 || 0
|}
Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs.
{|class="wikitable"
|+ Masque par instruction, associé au vecteur de disponibilité précédent
|-
! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY
|-
! ADD
| 1 || 1 || 0 || 0 || 0
|-
! MUL
| 0 || 0 || 1 || 0 || 0
|-
! COMP
| 0 || 0 || 0 || 1 || 0
|-
! LOAD
| 0 || 0 || 0 || 0 || 1
|-
! STORE
| 0 || 0 || 0 || 0 || 1
|-
|}
Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0.
Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire.
==L'émission et les micro-opérations mémoire==
Les micro-opérations mémoires sont gérées par l''''unité mémoire''', ou encore la ''Load-Store Unit''. Nous utiliserons le terme unité mémoire dans la suite du cours. L'unité mémoire est pour le moment une sorte de boite noire, dont on ne détaillera pas l'intérieur, et dont seules les entrées et sorties nous sont accessibles. L'unité d'émission la considère d'ailleurs comme telle. L'unité mémoire prend en entrée des opérandes, à savoir les adresses à lire/écrire, éventuellement la donnée à écrire. En sortie, elle fournit la donnée lue et le registre de destination dans lequel enregistrer la donnée. Les entrées et sorties sont reliées au réseau de contournement, ainsi qu'aux registres.
En général, l'adresse est lue dans les registres, mais elle peut aussi être fournie par le réseau de contournement. Mais certains modes d'adressage font que l'adresse doit être calculée. L'adressage base + indice est un bon exemple : il demande de calculer l'adresse à lire/écrire avec une addition et éventuellement un décalage. Dans le chapitre sur le pipeline, nous avons vu que les calculs d'adresse peuvent être effectués soit dans les unités de calcul entières, soit dans l'unité mémoire elle-même. Et les deux solutions ne sont pas équivalentes, elles ont chacune leurs avantages et leurs défauts. Un des avantages est lié au décodage des instructions, à savoir si les instructions mémoire sont décodées en une ou deux micro-opérations.
La solution la plus simple est celle qui effectue les calculs d'adresse dans l'unité mémoire elle-même. L'interface de l'unité mémoire est modifiée, de manière à pouvoir lui envoyer les indices et décalages nécessaires au calcul d'adresse. Si l'unité mémoire effectue les calculs d'adresse, les instructions mémoire sont décodées en une seule micro-opération, qui est envoyée à l'unité mémoire. Pas besoin de micro-opération séparée pour le calcul d'adresse. Le défaut de cette méthode est que l'unité mémoire doit incorporer une unité de calcul d'adresse, ce qui ajoute quelques circuits redondants avec l'ALU entière. Mais les circuits ajoutés se résument à des additionneurs et un circuit de décalage, ce qui est très léger. De nos jours, tous les processeurs font les calculs d'adresse dans l'unité mémoire. Le fait que le décodage soit simplifié l'emporte haut la main, comparé aux économies liées à l'unité de calcul d'adresse.
[[File:Unité mémoire dans un pipeline.png|centre|vignette|upright=2.5|Unité mémoire avec calcul d'adresse intégré, dans un pipeline.]]
Vous vous dites sans doute que les accès mémoire sont des instructions multicycles, et ce n'est pas faux. Mais elles se distinguent des autres instructions multicycle par leur temps d'exécution variable. Un accès au cache peut prendre deux à trois cycles d’horloge, un accès au cache L2 prend facilement 10 cycles, et un accès en RAM une centaine. Vu que leur latence n'est pas connue à l'avance, les techniques utilisées sur les pipelines multicycle/dynamiques ne fonctionnent pas parfaitement. La gestion des dépendances de données est notamment totalement différente.
Gérer les accès mémoire demande, entre autres, de gérer la disponibilité de l'unité mémoire. Mais contrairement aux instructions de calcul, les accès mémoire ont pour défaut qu'ils ont un nombre de cycles variable. Sur la plupart des processeurs, l'accès au cache se fait rapidement, mais l'accès à la mémoire est très lent. Et l'unité d'émission doit gérer la disponibilité de l'unité mémoire.
===Les unités mémoires bloquantes===
Dans ce qui suit, nous partons du principe que l'unité mémoire ne peut faire qu'un seul accès mémoire à la fois. Hypothèse crédible, mais certains processeurs avec exécution dans le désordre fonctionnent autrement. L'unité mémoire prévient elle-même de sa disponibilité. Elle est reliée à l'unité d'émission via un fil MEM_UNIT_READY qui prévient qu'elle est disponible et peut accepter un nouvel accès mémoire. Si le bit MEM_UNIT_READY est à 1, l'unité mémoire est inutilisé, il n'y a pas d'accès mémoire en cours. Par contre, si ce bit est à 0, alors un accès mémoire est en cours et l'unité mémoire ne peut pas accepter de nouvel accès mémoire.
L'unité d'émission tient en compte de ce bit pour décider de l'émission des instructions en cours. De plus, elle l'utilise pour déterminer si le registre de destination est disponible. Quand le signal MEM_UNIT_READY passe de 0 à 1, le no/numéro du registre destination est disponible en sortie de l'unité mémoire. Il est alors envoyé à l'unité d'émission et le registre de disponibilité est mis à jour.
[[File:Implémentation du stall lors d'un accès mémoire dans l'unité d'émission.png|centre|vignette|upright=2|Implémentation du stall lors d'un accès mémoire dans l'unité d'émission]]
Pour éviter tout problème, l'idéal est de stopper l'exécution d'autres instructions tant qu'un accès mémoire a lieu. Les instructions qui suivent l'accès mémoire sont mises en attente tant qu'il n'est pas terminé. En clair, on bloque le pipeline, l'unité d'émission cesse d'émettre des instructions et émet juste des bulles de pipeline. Pour cela, dès que l'unité d'émission émet une micro-opération mémoire, elle émet des bulles de pipeline à sa suite tant que le signal MEM_UNIT_READY est à 0. Une fois l'accès mémoire terminé, l'unité d'accès mémoire met le signal MEM_UNIT_READY à 1, ce qui prévient l'unité d'émission. L'émission des instructions reprend. C'est la solution la plus simple, mais elle est imparfaite.
===Les lectures non-bloquantes===
Les '''lectures non-bloquante''' sont une optimisation qui évite de bloquer le pipeline en cas d'accès mémoire. Elle a été utilisée sur le processeur ARM Cortex A53 et sur son successeur, l'ARM Cortex A510, avec un certain succès. L'idée est de continuer à faire des calculs en parallèle de l'accès mémoire, sous condition que cela ne pose pas de problèmes. Les instructions exécutées en parallèle sont exécutées dans l'unité de calcul, nous allons donc les appeler des opérations. L'unité d'émission ne peut pas émettre de nouvel accès mémoire, mais peut exécuter des opérations en parallèle de la lecture, si elles sont indépendantes de la lecture. Reste à savoir sous quelles conditions les opérations sont indépendants de la lecture.
La technique des lectures non-bloquantes ne se préoccupe pas des dépendances d'adresses. En effet, elles sont gérées par l'unité mémoire, pas par l'unité d'émission. Elle ne se préoccupe que des dépendances de registre. Les lectures et écritures lisent des opérandes dans les registres et ne sont pas émises tant qu'il y a une dépendance avec ces registres. Mais ce qui nous intéresse est ce qui se passe une fois la lecture/écriture émise. Une fois émises, les écritures n'écrivent dans aucun registre et n'ont donc pas de dépendances de registre. Par contre, les lectures écrivent la donnée lue dans un registre de destination, ce qui peut faire apparaitre des dépendances.
Le premier problème est qu'il se peut qu'une opération utilise la donnée lue par l'accès mémoire. Par exemple, la lecture charge une donnée dans un registre et ce registre est ensuite utilisé comme opérande d'une addition. Dans ce cas, l'opération ne doit pas s’exécuter tant que la donnée n'a pas été chargée. Un autre cas est celui où une opération écrit dans le registre de destination de la lecture. Avec des lectures non-bloquantes, l'opération ne doit pas démarrer tant que la lecture n'est pas terminée. Les dépendances pertinentes ici sont des dépendances avec le registre de destination de la lecture.
L'unité d'émission vérifie les dépendances avec le registre de destination de la lecture. Pour cela, l'unité d'émission marque le registre de destination de la lecture comme « réservé », du moins tant que la lecture n'a pas écrit son résultat dedans. Lorsqu'elle émet une lecture, l'unité d'émission marque le registre de destination comme réservé. Elle continue d’émettre des instructions si elles sont indépendantes de ce registre destination, mais bloque l'émission de toute instruction qui l'utilise. Ainsi, les instructions qui ne lisent ou n'écrivent pas ce registre peuvent s'exécuter dans les unités de calcul.
L'implémentation de cette technique demande juste de pouvoir réserver des registres, et consulter les réservations à chaque cycle. Pour marquer les registres comme réservés/non-réservé, l'unité d'émission contient un ''bit de réservation en lecture'' par registre. Chaque bit indique si le registre associé contient une donnée valide ou non. Le bit de validité est automatiquement comme invalide quand le processeur démarre une lecture. Le tout est regroupé dans un '''registre de réservation'''. Le registre de réservation peut être fusionné avec le registre de disponibilité vu plus haut.
Toute instruction qui a un registre invalide comme opérande est mise en attente. Idem pour les instructions qui ont un registre invalide comme registre de destination. À chaque cycle, l'unité d'émission tente de démarrer une nouvelle instruction et vérifie si elle est dépendante de la lecture. Elle vérifie si l'instruction à émettre a le registre de destination de la lecture comme opérande, ou comme registre de destination. Si c'est le cas, alors l'instruction est bloquée dans le pipeline. Mais dans le cas contraire, l'instruction de calcul est indépendante de l'accès mémoire, et peuvent démarrer sans trop de problèmes.
===L'exécution dans le désordre et autres alternatives===
Les lectures non-bloquantes peuvent être vues comme une sorte d'exécution dans le désordre limitée aux lectures. La différence avec l'exécution dans le désordre est que la technique des lectures non-bloquantes détecte si une instruction est dépendante d'une lecture, alors que l’exécution dans le désordre détectent toutes les dépendances entre instructions, pas seulement les dépendances avec un accès mémoire.
Le processeur ROCK, annulé en 2010, utilisait une version améliorée des lectures non-bloquantes, appelée la technique des '''éclaireurs matériels'''. La différence est que le processeur ne stoppe pas quand il rencontre une instruction dépendante de la lecture. À la place, les instructions dépendantes de la lecture sont mises en attente dans une file d'attente spécialisée, appelée la '''file d’attente différée''' (''deferred queue''), qui est une mémoire FIFO un peu modifiée. Une fois que la lecture renvoie un résultat, le processeur cesse l’exécution des instructions et exécute les instructions mises en attente. Les instructions mises en attente sont exécutées dans l'ordre, avant de reprendre là où le programme s'était arrêté.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les pipelines multicycles
| prevText=Les pipelines multicycles
| next=Le contournement (data forwarding)
| nextText=Le contournement (data forwarding)
}}
</noinclude>
ee70ksuveoglybo27ej8qwjto3ikzbn
774567
774530
2026-10-09T21:10:10Z
Mewtow
31375
774567
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir les processeurs à émission dans l'ordre et allons parler longuement des '''dépendances de données'''. Pour rappel, deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) au même registre ou à la même adresse mémoire. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. C'est le cas le plus évident, mais on a aussi des cas moins intuitifs. Voyons lesquels et comment ceux-ci sont gérés par le pipeline.
==Les dépendances de données==
Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c'est à dire au même registre ou la même adresse mémoire. En tout, cela fait quatre possibilités :
* '''Read after read''' : Deux instructions lisent la même donnée, mais pas en même temps.
* '''Read after write''' : Une instruction a pour opérande le résultat d'une autre : elle lit le résultat écrit par la première.
* '''Write after write''' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, mais pas en même temps.
* '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Pour les dépendances ''Read after read'', on peut mettre les deux instructions dans n'importe quel ordre, cela ne pose aucun problème. Ce ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, ce n'est pas le cas avec les trois autres types de dépendances, qui imposent que la première instruction s'exécute avant la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* '''Read after write''' : La première instruction doit écrire son résultat avant que la seconde le lise.
* '''Write after write''' : La première instruction doit écrire son résultat avant la seconde.
* '''Write after read''' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
: Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW.
===Les dépendances de registre et mémoire===
Une autre distinction sépare les dépendances de registre et les dépendances d'adresse. Une dépendance de donnée survient quand deux instructions lisent/écrivent le même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions. Dans la suite du cours, nous allons supposer une architecture LOAD-STORE dans la plupart des cas. Une telle architecture a une séparation stricte entre accès mémoire et les autres instructions. Les accès mémoire regroupent les instructions LOAD et STORE, qui se distinguent des autres instructions. Pour simplifier, les autres instructions seront appelées des ''opérations'', ou encore des instructions de calcul.
Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Les '''dépendances d'adresse''' surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Là encore, on a le cas des dépendances RAW, WAW et WAR.
Les dépendances d'adresse se manifestent sur tous les pipelines, même ceux qui ne sont pas multicycles. La raison est que les écritures se font à la toute fin du pipeline, pour gérer les exceptions précises, alors que les lectures se font quelques étages avant. Il peut alors y avoir quelques problèmes, lorsque plusieurs instructions mémoire se suivent. Imaginez par exemple qu'une lecture s'exécute avant qu'une écriture atteigne la fin du pipeline. Si les deux instructions accèdent à la même donnée, on a une dépendance d'adresse de type RAW. Le cas sera étudié dans le chapitre sur le contournement.
Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites. Et autant les registres lus/écrit sont connus dès que l'instruction est décodée, autant les adresses sont souvent calculées à l'exécution ou mémorisées dans des registres. Les deux types de dépendances sont donc le fait de circuits séparés, qui sont dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité d'accès mémoire et notamment de la file d'écriture, les autres dépendances sont gérées pendant/après l'étape de décodage d'instruction. Aussi, il est important de séparer les deux le plus possible.
===Les dépendances de registre===
Dans ce qui va suivre, nous allons nous limiter aux dépendances liées aux registres, et mettre de côté les dépendances d'adresse. Intuitivement, on se dit que de les dépendances de registres concernent les instructions arithmétiques, mais les instructions d'accès mémoire peuvent avoir des dépendances de registre avec d'autres instructions. L'instruction STORE lit la donnée à écrire et son adresse dans les registres, elle a donc potentiellement des dépendances de registre, à cause des registre opérande. Si une autre instruction lit/écrit ces registres opérandes, une dépendance de registre survient. L'instruction LOAD est dans un cas similaire, : elle récupère l'adresse à lire dans un registre opérande (la plupart du temps), et écrit la donnée lue dans un autre registre de destination. Ce qui fait deux sources de dépendances de registre : le registre opérande, le registre de destination. Si une instruction de calcul ou une autre instruction mémoire lit/écrit dans ces registres, une dépendance de donnée a lieu.
Dans ce chapitre, nous allons nous préoccuper des trois dépendances de registres suivantes :
* '''''Read after write''''' : Une instruction lit un registre dans laquelle l'instruction précédente a écrit.
* '''''Write after read''''' : Une instruction lit un registre pour récupérer une opérande, mais ce registre est ensuite écrasé par une instruction ultérieure.
* '''''Write after write''''' : Deux instructions écrivent dans le même registre, mais à des instants différents.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW.
Les dépendances RAW et WAW, imposent que la première instruction ait écrit son résultat avant que la seconde lise ses opérandes. Si on ne fait pas ça, la seconde instruction lit des opérandes invalides ou voit son écriture écrasée par une écriture précédente. Pour éviter cela, la seconde instruction doit être démarrée quand la première s'est totalement terminée. Bloquer le pipeline est donc une nécessité. Mais diverses optimisations permettent de mitiger l'impact des dépendances de données. Par exemple, dans le prochain chapitre, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. Les dépendances WAW et WAR peuvent aussi être totalement supprimées par des techniques de renommages de registres, qui auront leur chapitre dédié.
===Les dépendances présentes dépendent du pipeline===
Le support des exceptions précises supprime pas mal de dépendances de données. Le fait que les écritures mémoire garantit que les écritures se fassent dans l'ordre du programme, il n'y a pas de situations où une écriture soit exécutée avant une écriture antérieure. Et cela suffit à faire disparaitre les dépendances WAW. De même, les dépendances d'adresse WAR sont impossibles avec des exceptions précises, car les écritures se font à la fin du pipeline, après les lectures.
Même sans exceptions précises, les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. En pratique, les dépendances WAR n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW, quant à elles, n'apparaissent que sur les pipelines dynamiques, ceux qui supportent des instructions multicycles, ou si les écritures prennent plusieurs étages.
{|class="wikitable"
|-
! Dépendance
! Pipeline
! colspan="2" | Solutions
|-
! RAW
| Tous les pipelines
| rowspan="3" | Bloquer le pipeline
| Contournement
|-
! WAW
| Pipelines dynamiques
| rowspan="2" | Renommage de registres
|-
! WAR
| Pipelines dynamiques avec exécution dans le désordre
|}
Les dépendances WAR et WAW sont souvent appelées des dépendances de nom (''naming dependency'') et sont opposées aux dépendances vraies (''true dependency''), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Et on peut les faire disparaitre avec des techniques dites de renommage de registre, qui feront l'objet d'un chapitre ultérieur.
==L'émission d'une instruction==
Les dépendances imposent qu'une première instruction soit totalement terminée avant qu'on démarre la seconde, ou du moins qu'elle ait progressé suffisamment dans le pipeline. Pour cela, il suffit de retarder l'exécution de la seconde instruction tant que les conditions nécessaires pour son exécution ne sont pas réunies. Par exemple, prenons le cas où une instruction a pour opérande le résultat d'une autre : la seconde doit attendre que le résultat de la première soit calculé avant de démarrer, il se peut qu'elle soit retardée durant quelques cycles pour cela.
Les processeurs détectent les dépendances de registre dans un circuit spécialisé, appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée. Si une instruction a une dépendance bloquante, elle est bloquée dans l'étage d'émission en attendant de pouvoir s'exécuter. Durant ce temps d'attente, on insère des vides après l'unité d'émission : certains étages seront inoccupés et n'auront rien à faire. Les étages qui sont situés avant l'étage d'émission sont eux bloqués, la progression des instructions dedans est stoppée.
[[File:Pipeline bubble.png|centre|vignette|upright=2|Pipeline bubble]]
Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données.
Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline, le plus possible. D'ailleurs, nous passerons les prochains chapitres à parler de ces optimisations.
===La détection des dépendances de données===
Le processeur doit détecter les dépendances de données entre instructions. Les dépendances de registres se détectent en comparant entre eux les registres utilisés par les instructions. Les registres étant connus dès la sortie de l'étape de décodage, la détection des dépendances de données se fait de manière précoce, avant même l'exécution des instructions. Une sacré différence avec les dépendances d'adresse, qui demandent que les adresses soient calculées et lues dans les registres avant qu'on puisse détecter les dépendances.
La détection des dépendances de registre se fait dans l'étage d'émission, situé juste après l'étage de décodage d'instruction. L'unité d'émission reçoit une instruction décodée et vérifie ses dépendances avec les instructions dans le pipeline. À chaque cycle d'horloge, l'unité d'émission analyse une instruction décodée appelée '''instruction candidate'''. Elle détecte si cette instruction candidate a une dépendance avec une instruction déjà dans le pipeline. Les instructions dans le pipeline n'ont pas encore enregistrées leurs résultats dans les registres, soit parce qu'elles sont en cours d'exécution, qu'elles lisent leurs opérandes, peut importe. Elles seront appelées '''instructions "en vol"'''.
Pour cela, l'unité d'émission compare les registres utilisés par l'instruction candidate et les instructions dans le pipeline. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Les quatre situations possibles sont les suivantes :
* Un des registres opérande est à lire (opérande) par une instruction dans le pipeline : pas de dépendance.
* Un des registres opérande est écrit (destination) par une instruction dans le pipeline : dépendance RAW.
* Un des registres de destination est à lire (opérande) par une instruction dans le pipeline : potentielle dépendance WAR, mais seulement sur des pipelines pathologiques jamais utilisés.
* Un des registres de destination est écrit (destination) par une instruction dans le pipeline : dépendance WAW.
Notez que pour les lectures, on part du principe que les instructions en vol n'ont pas encore lu leurs registres opérandes. Dès qu'elles ont lu les opérandes en question, les dépendances liées disparaissent. Pareil pour les écritures, sauf que celles-ci ont lieu à la toute fin du pipeline. Les écritures en attente ont donc lieu quand l'instruction termine.
{|class="wikitable"
|-
! rowspan="2" colspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! rowspan="2" | Instructions en vol
! Registres pas encore lus (opérandes)
| Pas de dépendances (dépendance RAR inoffensive)
| Dépendance WAR potentielle, pas en pratique
|-
! Registre pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
L'algorithme est donc le suivant : une lecture est autorisée en absence d'écriture (dans le même registre), une écriture est autorisée en absence de lecture (dans le même registre). Cependant, on peut grandement simplifier l'algorithme et ne pas tenir compte des dépendances WAR, qui n'existent pas sur la plupart des pipelines dynamiques sans exécution dans le désordre. Le tableau se simplifie en retirant les dépendances inutiles, seule la dernière ligne nous intéresse au final.
{|class="wikitable"
|-
! rowspan="2" colspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! Instructions en vol
! Registre pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
===L'exécution dans l'ordre et dans le désordre des instructions===
Nous venons de voir que les dépendances de données posent des problèmes qu'un pipeline de longueur variable doit résoudre. La solution la plus simple retarde les instructions candidates problématiques de quelques cycles d'horloges. Si une instruction a une dépendance problématique avec une autre, il suffit simplement de l’exécuter avec un retard de cycles. Pour cela, lorsqu'une instruction multicycle est démarrée, les instructions problématiques sont bloquées à l'étage de décodage en attendant le bon moment. Pour cela, on retarde l'instruction fautive dans l'unité de décodage.
[[File:Pipeline bubble.png|centre|vignette|upright=2|Bulle de pipeline.]]
Notons que cette technique exécute des instructions indépendantes dans des unités de calcul séparées. Dans l'exemple précédent avec une instruction multicycle suivie par une instruction normale, le processeur détecte les situations où ce n'est pas grave si la seconde instruction finisse avant la première : on peut alors lancer la seconde instruction immédiatement sans attendre. Il y a donc un gain en performance. Un point important est que cette technique émet les instructions dans l'ordre, mais qu'elles peuvent s'exécuter dans le désordre et terminer dans le désordre. En clair, les instructions sont injectées dans les unités de calcul dans l'ordre, mais en sortent dans le désordre.
Pour éviter de bloquer le pipeline, les processeurs modernes incorporent des optimisations assez intéressantes. Nous verrons la technique du contournement dans le chapitre suivant. Elle permet de ne pas bloquer le pipeline en cas de dépendance ''Read after write''. Une solution complémentaire est ce qu'on appelle l''''exécution dans le désordre''', une optimisation commune sur les processeurs modernes qui sera vue en détail dans plusieurs chapitres dans la suite du cours.
L'idée est que si une instruction problématique est bloquée dans l'unité de décodage/émission, elle est juste mise en attente, elle ne bloque pas le pipeline. Le processeur regarde les instructions suivantes et vérifie si certaines d'entre elles peuvent s'exécuter. Les instructions mises en attente sont exécutées dès que possible, elles ont la priorité. La gestion de la mise en attente des instructions problématiques est assez complexe et implique de les stocker dans des mémoires spécifiques. Là encore, il faut que les instructions exécutées en parallèle ne manipulent pas les mêmes données, ni les mêmes registres, ni les mêmes adresses mémoires.
La toute première implémentation de l'exécution dans le désordre était sur l'ordinateur CDC 6600, suivi par son successeur le CDC 7600. Il intégrait un circuit appelé le '''''scoreboard''''', qui gérait l'exécution dans le désordre. Décrire le circuit en question est assez compliqué, aussi je le laisse de côté pour le moment. Sachez le terme ''scoreboard'' est resté et a été élargit à tout circuit d'émission, qu'il soit dans le désordre ou dans l'ordre.
==L'étage d'émission : le blocage du pipeline==
Maintenant, voyons comment est implémenté l'étage d'émission (''issue''), qui détecte les situations problématiques et bloque le pipeline si besoin. Il suit immédiatement l'étage de décodage, et est situé avant le chemin de données. Le rôle de l'unité d'émission est de générer de bloquer le pipeline. Si elle reçoit une instruction problématique, elle la bloque dans l'unité d'émission et bloque le pipeline tant que la dépendance n'est pas réglée. Mais si l'instruction peut s'exécuter, elle l'envoie dans la suite du pipeline. On dit qu'elle émet l'instruction.
L''''émission''' est donc l'envoi d'une micro-opération dans le pipeline, si les conditions sont réunies. Un point important de l'émission est que le processeur émet une instruction par cycle, en absence de dépendance de données, mais aussi en absence de dépendances structurelles, les deux étant détectées par l'unité d'émission.
===La gestion des dépendances de données===
Pour rappel, l'unité d'émission détecte les dépendances entre une '''instruction candidate''' et les instructions déjà dans le pipeline, aussi appelées '''instructions "en vol"'''. L'unité d'émission détecte les dépendances entre l'instruction candidate et chaque instruction en vol. Pour rappel, les dépendances de données se détectent en comparant les instructions de deux opérandes. Vu que les dépendances WAR sont très rares, et n'existent pas sur les pipelines normaux, on ne tient compte que des dépendances RAW et WAW. Les détecter demande de comparer les registres de l'instruction candidate, avec les registres de destination des instructions en vol.
{|class="wikitable"
|-
! rowspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! Instructions en vol
Registres pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
L'unité d'émission doit connaitre les registres de destination des instructions en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut bloquer le pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons.
[[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]]
Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être écrit par une autre instruction. Les registres qui ne sont pas réservés sont dit libres ou disponibles. Si une instruction candidate veut lire ce registre, c'est signe qu'il y a une dépendance RAW : elle veut lire un registre réservé, qui est destiné à être écrit par une instruction en vol. Si elle veut écrire, c'est signe qu'elle veut écrire dans un registre où une autre instruction veut écrire : c'est une dépendance WAW. Dans les deux cas, l'instruction n'est pas émise.
Le registre encode les registres réservés en représentation ''one-hot'' modifiée. Chaque bit est associé à un registre. Le bit numéro 0 est associé au registre numéro 0, le bit numéro 1 au registre numéro 1, etc. Un registre est réservé en mettant le bit associé à 1.
{|class="wikitable"
|+ Registre de réservation
|-
! Registre 7 !! Registre 6 !! Registre 5 !! Registre 4 !! Registre 3 !! Registre 2 !! Registre 1 !! Registre 0
|-
| 0 || 0 || 1 || 0 || 0 || 1 || 0 || 0
|}
Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''.
Avant d'émettre une instruction, l'unité d'émission extrait les registres opérande/destination et les convertis dans la même représentation que le registre de réservation, à savoir en représentation ''one-hot''. Le circuit de traduction binaire vers ''one-hot'' est, pour rappel, un simple décodeur. Puis, on fait un OU logique entre les sorties des décodeurs. Le résultat est un masque qui indique quels registres sont lus par l'instruction, encodé en représentation ''one-hot''. Appelons-le le '''masque d'opérandes'''. Le masque est alors comparé au registre de réservation pour vérifier si l'instruction peut être émise.
[[File:Unité d'émission simple, dans l'ordre.png|centre|vignette|upright=2|Unité d'émission simple, dans l'ordre]]
Si l'émission est autorisée, le registre de réservation est là aussi mis à jour. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. Le registre de réservationest aussi mis à jour dès qu'une instruction enregistre son résultat dans les registres, ou alors dès qu'il est disponible pour le contournement. Le bit associé au registre repasse alors à 0 (ou à 1). Sans contournement, la mise à jour se fait alors à la toute fin de l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul.
Vous aurez peut-être remarqué, mais une différence est que les pipelines fixes retardent les écritures dans le banc de registre avec des registres d'échelonnage, les pipelines dynamique retardent l'émission des instructions. Les deux méthodes ont sensiblement le même résultat.
Le '''registre de consultation''' permet de désigner les registres qui vont être lus par les instructions en vol. Je précise bien : les registres qui n'ont pas encore été lus. Les registres en question sont dit consulté, les autres sont dit non-consultés. Il est interdit d'écrire dans un registre consulté, c'est signe d'une dépendance WAR. Il faut noter que beaucoup de pipeline s'en passent car ils n'ont pas de dépendances WAR. Le registre de consultation est mis à jour dès que l'opérande est lue. Et cela arrive assez tôt dans le pipeline, au bout de quelques cycles grand maximum. Sur beaucoup de pipelines, il n'est pas présent dans il est impossible qu'une écriture ait lieu avant qu'une lecture ait démarrée. Mais sur les pipelines où de telles dépendances sont possibles, il faut en tenir compte.
Il existe quelques rares processeurs qui fusionnent le registre de réservation et de consultation en un seul. Le registre contient autant de bits que de registres, chaque bit indique si le registre associé est pas encore lu/écrit. De tels implémentations détectent alors toutes les dépendances, même les dépendances ''Read After Read''. Elles tendent à être très conservatrices. Un exemple est le co-processeur vectoriel VFP9-S d'ARM. Leur implémentation est simple, mais il peut y avoir un cout en performance.
===La gestion de la disponibilité des ALUs===
L'étage d'émission détecte aussi les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles.
Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline.
Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''.
{|class="wikitable"
|+ Vecteur de disponibilité des ALUs
|-
! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY
|-
| 0 || 0 || 1 || 0 || 0
|}
Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs.
{|class="wikitable"
|+ Masque par instruction, associé au vecteur de disponibilité précédent
|-
! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY
|-
! ADD
| 1 || 1 || 0 || 0 || 0
|-
! MUL
| 0 || 0 || 1 || 0 || 0
|-
! COMP
| 0 || 0 || 0 || 1 || 0
|-
! LOAD
| 0 || 0 || 0 || 0 || 1
|-
! STORE
| 0 || 0 || 0 || 0 || 1
|-
|}
Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0.
Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire.
==L'émission et les micro-opérations mémoire==
Les micro-opérations mémoires sont gérées par l''''unité mémoire''', ou encore la ''Load-Store Unit''. Nous utiliserons le terme unité mémoire dans la suite du cours. L'unité mémoire est pour le moment une sorte de boite noire, dont on ne détaillera pas l'intérieur, et dont seules les entrées et sorties nous sont accessibles. L'unité d'émission la considère d'ailleurs comme telle. L'unité mémoire prend en entrée des opérandes, à savoir les adresses à lire/écrire, éventuellement la donnée à écrire. En sortie, elle fournit la donnée lue et le registre de destination dans lequel enregistrer la donnée. Les entrées et sorties sont reliées au réseau de contournement, ainsi qu'aux registres.
En général, l'adresse est lue dans les registres, mais elle peut aussi être fournie par le réseau de contournement. Mais certains modes d'adressage font que l'adresse doit être calculée. L'adressage base + indice est un bon exemple : il demande de calculer l'adresse à lire/écrire avec une addition et éventuellement un décalage. Dans le chapitre sur le pipeline, nous avons vu que les calculs d'adresse peuvent être effectués soit dans les unités de calcul entières, soit dans l'unité mémoire elle-même. Et les deux solutions ne sont pas équivalentes, elles ont chacune leurs avantages et leurs défauts. Un des avantages est lié au décodage des instructions, à savoir si les instructions mémoire sont décodées en une ou deux micro-opérations.
La solution la plus simple est celle qui effectue les calculs d'adresse dans l'unité mémoire elle-même. L'interface de l'unité mémoire est modifiée, de manière à pouvoir lui envoyer les indices et décalages nécessaires au calcul d'adresse. Si l'unité mémoire effectue les calculs d'adresse, les instructions mémoire sont décodées en une seule micro-opération, qui est envoyée à l'unité mémoire. Pas besoin de micro-opération séparée pour le calcul d'adresse. Le défaut de cette méthode est que l'unité mémoire doit incorporer une unité de calcul d'adresse, ce qui ajoute quelques circuits redondants avec l'ALU entière. Mais les circuits ajoutés se résument à des additionneurs et un circuit de décalage, ce qui est très léger. De nos jours, tous les processeurs font les calculs d'adresse dans l'unité mémoire. Le fait que le décodage soit simplifié l'emporte haut la main, comparé aux économies liées à l'unité de calcul d'adresse.
[[File:Unité mémoire dans un pipeline.png|centre|vignette|upright=2.5|Unité mémoire avec calcul d'adresse intégré, dans un pipeline.]]
Vous vous dites sans doute que les accès mémoire sont des instructions multicycles, et ce n'est pas faux. Mais elles se distinguent des autres instructions multicycle par leur temps d'exécution variable. Un accès au cache peut prendre deux à trois cycles d’horloge, un accès au cache L2 prend facilement 10 cycles, et un accès en RAM une centaine. Vu que leur latence n'est pas connue à l'avance, les techniques utilisées sur les pipelines multicycle/dynamiques ne fonctionnent pas parfaitement. La gestion des dépendances de données est notamment totalement différente.
Gérer les accès mémoire demande, entre autres, de gérer la disponibilité de l'unité mémoire. Mais contrairement aux instructions de calcul, les accès mémoire ont pour défaut qu'ils ont un nombre de cycles variable. Sur la plupart des processeurs, l'accès au cache se fait rapidement, mais l'accès à la mémoire est très lent. Et l'unité d'émission doit gérer la disponibilité de l'unité mémoire.
===Les unités mémoires bloquantes===
Dans ce qui suit, nous partons du principe que l'unité mémoire ne peut faire qu'un seul accès mémoire à la fois. Hypothèse crédible, mais certains processeurs avec exécution dans le désordre fonctionnent autrement. L'unité mémoire prévient elle-même de sa disponibilité. Elle est reliée à l'unité d'émission via un fil MEM_UNIT_READY qui prévient qu'elle est disponible et peut accepter un nouvel accès mémoire. Si le bit MEM_UNIT_READY est à 1, l'unité mémoire est inutilisé, il n'y a pas d'accès mémoire en cours. Par contre, si ce bit est à 0, alors un accès mémoire est en cours et l'unité mémoire ne peut pas accepter de nouvel accès mémoire.
L'unité d'émission tient en compte de ce bit pour décider de l'émission des instructions en cours. De plus, elle l'utilise pour déterminer si le registre de destination est disponible. Quand le signal MEM_UNIT_READY passe de 0 à 1, le no/numéro du registre destination est disponible en sortie de l'unité mémoire. Il est alors envoyé à l'unité d'émission et le registre de disponibilité est mis à jour.
[[File:Implémentation du stall lors d'un accès mémoire dans l'unité d'émission.png|centre|vignette|upright=2|Implémentation du stall lors d'un accès mémoire dans l'unité d'émission]]
Pour éviter tout problème, l'idéal est de stopper l'exécution d'autres instructions tant qu'un accès mémoire a lieu. Les instructions qui suivent l'accès mémoire sont mises en attente tant qu'il n'est pas terminé. En clair, on bloque le pipeline, l'unité d'émission cesse d'émettre des instructions et bloque le pipeline. Pour cela, dès que l'unité d'émission émet une micro-opération mémoire, elle bloque le pipeline tant que le signal MEM_UNIT_READY est à 0. Une fois l'accès mémoire terminé, l'unité d'accès mémoire met le signal MEM_UNIT_READY à 1, ce qui prévient l'unité d'émission. L'émission des instructions reprend. C'est la solution la plus simple, mais elle est imparfaite.
===Les lectures non-bloquantes===
Les '''lectures non-bloquante''' sont une optimisation qui évite de bloquer le pipeline en cas d'accès mémoire. Elle a été utilisée sur le processeur ARM Cortex A53 et sur son successeur, l'ARM Cortex A510, avec un certain succès. L'idée est de continuer à faire des calculs en parallèle de l'accès mémoire, sous condition que cela ne pose pas de problèmes. Les instructions exécutées en parallèle sont exécutées dans l'unité de calcul, nous allons donc les appeler des opérations. L'unité d'émission ne peut pas émettre de nouvel accès mémoire, mais peut exécuter des opérations en parallèle de la lecture, si elles sont indépendantes de la lecture. Reste à savoir sous quelles conditions les opérations sont indépendants de la lecture.
La technique des lectures non-bloquantes ne se préoccupe pas des dépendances d'adresses. En effet, elles sont gérées par l'unité mémoire, pas par l'unité d'émission. Elle ne se préoccupe que des dépendances de registre. Les lectures et écritures lisent des opérandes dans les registres et ne sont pas émises tant qu'il y a une dépendance avec ces registres. Mais ce qui nous intéresse est ce qui se passe une fois la lecture/écriture émise. Une fois émises, les écritures n'écrivent dans aucun registre et n'ont donc pas de dépendances de registre. Par contre, les lectures écrivent la donnée lue dans un registre de destination, ce qui peut faire apparaitre des dépendances.
Le premier problème est qu'il se peut qu'une opération utilise la donnée lue par l'accès mémoire. Par exemple, la lecture charge une donnée dans un registre et ce registre est ensuite utilisé comme opérande d'une addition. Dans ce cas, l'opération ne doit pas s’exécuter tant que la donnée n'a pas été chargée. Un autre cas est celui où une opération écrit dans le registre de destination de la lecture. Avec des lectures non-bloquantes, l'opération ne doit pas démarrer tant que la lecture n'est pas terminée. Les dépendances pertinentes ici sont des dépendances avec le registre de destination de la lecture.
L'unité d'émission vérifie les dépendances avec le registre de destination de la lecture. Pour cela, l'unité d'émission marque le registre de destination de la lecture comme « réservé », du moins tant que la lecture n'a pas écrit son résultat dedans. Lorsqu'elle émet une lecture, l'unité d'émission marque le registre de destination comme réservé. Elle continue d’émettre des instructions si elles sont indépendantes de ce registre destination, mais bloque l'émission de toute instruction qui l'utilise. Ainsi, les instructions qui ne lisent ou n'écrivent pas ce registre peuvent s'exécuter dans les unités de calcul.
L'implémentation de cette technique demande juste de pouvoir réserver des registres, et consulter les réservations à chaque cycle. Pour marquer les registres comme réservés/non-réservé, l'unité d'émission contient un ''bit de réservation en lecture'' par registre. Chaque bit indique si le registre associé contient une donnée valide ou non. Le bit de validité est automatiquement comme invalide quand le processeur démarre une lecture. Le tout est regroupé dans un '''registre de réservation'''. Le registre de réservation peut être fusionné avec le registre de disponibilité vu plus haut.
Toute instruction qui a un registre invalide comme opérande est mise en attente. Idem pour les instructions qui ont un registre invalide comme registre de destination. À chaque cycle, l'unité d'émission tente de démarrer une nouvelle instruction et vérifie si elle est dépendante de la lecture. Elle vérifie si l'instruction à émettre a le registre de destination de la lecture comme opérande, ou comme registre de destination. Si c'est le cas, alors l'instruction est bloquée dans le pipeline. Mais dans le cas contraire, l'instruction de calcul est indépendante de l'accès mémoire, et peuvent démarrer sans trop de problèmes.
===L'exécution dans le désordre et autres alternatives===
Les lectures non-bloquantes peuvent être vues comme une sorte d'exécution dans le désordre limitée aux lectures. La différence avec l'exécution dans le désordre est que la technique des lectures non-bloquantes détecte si une instruction est dépendante d'une lecture, alors que l’exécution dans le désordre détectent toutes les dépendances entre instructions, pas seulement les dépendances avec un accès mémoire.
Le processeur ROCK, annulé en 2010, utilisait une version améliorée des lectures non-bloquantes, appelée la technique des '''éclaireurs matériels'''. La différence est que le processeur ne stoppe pas quand il rencontre une instruction dépendante de la lecture. À la place, les instructions dépendantes de la lecture sont mises en attente dans une file d'attente spécialisée, appelée la '''file d’attente différée''' (''deferred queue''), qui est une mémoire FIFO un peu modifiée. Une fois que la lecture renvoie un résultat, le processeur cesse l’exécution des instructions et exécute les instructions mises en attente. Les instructions mises en attente sont exécutées dans l'ordre, avant de reprendre là où le programme s'était arrêté.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les pipelines multicycles
| prevText=Les pipelines multicycles
| next=Le contournement (data forwarding)
| nextText=Le contournement (data forwarding)
}}
</noinclude>
so68wyr913451lpqd6ch3kkda1y07mf
774585
774567
2026-10-10T11:56:03Z
Mewtow
31375
774585
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir les processeurs à émission dans l'ordre et allons parler longuement des '''dépendances de données'''. Pour rappel, deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) au même registre ou à la même adresse mémoire. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. C'est le cas le plus évident, mais on a aussi des cas moins intuitifs. Voyons lesquels et comment ceux-ci sont gérés par le pipeline.
Précision importante : les dépendances ne sont pertinentes que pour les instructions en vol, celles qui sont dans le chemin de données. Si une instruction a une dépendance avec une autre, mais que les deux ne sont pas dans le pipeline en même temps, la dépendance n'a aucune conséquence. Les dépendances ne posent un problème que si elles concernent deux instructions qui sont en vol.
==Les dépendances de données==
Deux instructions ont une dépendance de données quand elles accèdent (en lecture ou écriture) à la même donnée, c'est à dire au même registre ou la même adresse mémoire. En tout, cela fait quatre possibilités :
* '''Read after read''' : Deux instructions lisent la même donnée, mais pas en même temps.
* '''Read after write''' : Une instruction a pour opérande le résultat d'une autre : elle lit le résultat écrit par la première.
* '''Write after write''' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, mais pas en même temps.
* '''Write after read''' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Pour les dépendances ''Read after read'', on peut mettre les deux instructions dans n'importe quel ordre, cela ne pose aucun problème. Ce ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, ce n'est pas le cas avec les trois autres types de dépendances, qui imposent que la première instruction s'exécute avant la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* '''Read after write''' : La première instruction doit écrire son résultat avant que la seconde le lise.
* '''Write after write''' : La première instruction doit écrire son résultat avant la seconde.
* '''Write after read''' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
: Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW.
===Les dépendances de registre et mémoire===
Une autre distinction sépare les dépendances de registre et les dépendances d'adresse. Une dépendance de donnée survient quand deux instructions lisent/écrivent le même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions. Dans la suite du cours, nous allons supposer une architecture LOAD-STORE dans la plupart des cas. Une telle architecture a une séparation stricte entre accès mémoire et les autres instructions. Les accès mémoire regroupent les instructions LOAD et STORE, qui se distinguent des autres instructions. Pour simplifier, les autres instructions seront appelées des ''opérations'', ou encore des instructions de calcul.
Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Le cas typique est celui où une instruction lit un registre et qu'une autre écrit dedans, mais il faut aussi tenir compte du cas avec deux écritures dans le même registre. Les '''dépendances d'adresse''' surviennent quand deux instructions mémoire lisent/écrivent dans une même adresse mémoire. Là encore, on a le cas des dépendances RAW, WAW et WAR.
Les dépendances d'adresse se manifestent sur tous les pipelines, même ceux qui ne sont pas multicycles. La raison est que les écritures se font à la toute fin du pipeline, pour gérer les exceptions précises, alors que les lectures se font quelques étages avant. Il peut alors y avoir quelques problèmes, lorsque plusieurs instructions mémoire se suivent. Imaginez par exemple qu'une lecture s'exécute avant qu'une écriture atteigne la fin du pipeline. Si les deux instructions accèdent à la même donnée, on a une dépendance d'adresse de type RAW. Le cas sera étudié dans le chapitre sur le contournement.
Un point important est que la gestion des dépendances est différente entre les dépendances de registre et les dépendances d'adresse. Détecter une dépendance de registre demande de comparer les registres utilisés par deux instructions. Par contre, détecter les dépendances d'adresse demande de comparer les adresses lues/écrites. Et autant les registres lus/écrit sont connus dès que l'instruction est décodée, autant les adresses sont souvent calculées à l'exécution ou mémorisées dans des registres. Les deux types de dépendances sont donc le fait de circuits séparés, qui sont dans des étages de pipeline distincts. La gestion des dépendances d'adresse est le fait de l'unité d'accès mémoire et notamment de la file d'écriture, les autres dépendances sont gérées pendant/après l'étape de décodage d'instruction. Aussi, il est important de séparer les deux le plus possible.
===Les dépendances de registre===
Dans ce qui va suivre, nous allons nous limiter aux dépendances liées aux registres, et mettre de côté les dépendances d'adresse. Intuitivement, on se dit que de les dépendances de registres concernent les instructions arithmétiques, mais les instructions d'accès mémoire peuvent avoir des dépendances de registre avec d'autres instructions. L'instruction STORE lit la donnée à écrire et son adresse dans les registres, elle a donc potentiellement des dépendances de registre, à cause des registre opérande. Si une autre instruction lit/écrit ces registres opérandes, une dépendance de registre survient. L'instruction LOAD est dans un cas similaire, : elle récupère l'adresse à lire dans un registre opérande (la plupart du temps), et écrit la donnée lue dans un autre registre de destination. Ce qui fait deux sources de dépendances de registre : le registre opérande, le registre de destination. Si une instruction de calcul ou une autre instruction mémoire lit/écrit dans ces registres, une dépendance de donnée a lieu.
Dans ce chapitre, nous allons nous préoccuper des trois dépendances de registres suivantes :
* '''''Read after write''''' : Une instruction lit un registre dans laquelle l'instruction précédente a écrit.
* '''''Write after read''''' : Une instruction lit un registre pour récupérer une opérande, mais ce registre est ensuite écrasé par une instruction ultérieure.
* '''''Write after write''''' : Deux instructions écrivent dans le même registre, mais à des instants différents.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW.
Les dépendances RAW et WAW, imposent que la première instruction ait écrit son résultat avant que la seconde lise ses opérandes. Si on ne fait pas ça, la seconde instruction lit des opérandes invalides ou voit son écriture écrasée par une écriture précédente. Pour éviter cela, la seconde instruction doit être démarrée quand la première s'est totalement terminée. Bloquer le pipeline est donc une nécessité. Mais diverses optimisations permettent de mitiger l'impact des dépendances de données. Par exemple, dans le prochain chapitre, nous parlerons de la technique du contournement, qui permet de ne pas bloquer le pipeline pour les dépendances RAW. Les dépendances WAW et WAR peuvent aussi être totalement supprimées par des techniques de renommages de registres, qui auront leur chapitre dédié.
===Les dépendances présentes dépendent du pipeline===
Le support des exceptions précises supprime pas mal de dépendances de données. Le fait que les écritures mémoire garantit que les écritures se fassent dans l'ordre du programme, il n'y a pas de situations où une écriture soit exécutée avant une écriture antérieure. Et cela suffit à faire disparaitre les dépendances WAW. De même, les dépendances d'adresse WAR sont impossibles avec des exceptions précises, car les écritures se font à la fin du pipeline, après les lectures.
Même sans exceptions précises, les dépendances WAR n'existent tout simplement pas sur la plupart des pipelines. En pratique, les dépendances WAR n'apparaissent que sur les processeurs dits à exécution dans le désordre, et encore, seulement si on n'utilise pas de renommage de registre, ce qui n'est plus le cas depuis des décennies. Les dépendances WAW, quant à elles, n'apparaissent que sur les pipelines dynamiques, ceux qui supportent des instructions multicycles, ou si les écritures prennent plusieurs étages.
{|class="wikitable"
|-
! Dépendance
! Pipeline
! colspan="2" | Solutions
|-
! RAW
| Tous les pipelines
| rowspan="3" | Bloquer le pipeline
| Contournement
|-
! WAW
| Pipelines dynamiques
| rowspan="2" | Renommage de registres
|-
! WAR
| Pipelines dynamiques avec exécution dans le désordre
|}
Les dépendances WAR et WAW sont souvent appelées des dépendances de nom (''naming dependency'') et sont opposées aux dépendances vraies (''true dependency''), à savoir les dépendances RAW. Les dépendances WAR et WAW viennent du fait qu'un même registre nommé est réutilisé plusieurs fois pour des données différentes. Elles n'existeraient pas si on utilisait un registre pour chaque donnée, qui est écrit une fois, lu une ou plusieurs fois, mais jamais écrasé. Et on peut les faire disparaitre avec des techniques dites de renommage de registre, qui feront l'objet d'un chapitre ultérieur.
==L'émission d'une instruction==
Les dépendances imposent qu'une première instruction soit totalement terminée avant qu'on démarre la seconde, ou du moins qu'elle ait progressé suffisamment dans le pipeline. Pour cela, il suffit de retarder l'exécution de la seconde instruction tant que les conditions nécessaires pour son exécution ne sont pas réunies. Par exemple, prenons le cas où une instruction a pour opérande le résultat d'une autre : la seconde doit attendre que le résultat de la première soit calculé avant de démarrer, il se peut qu'elle soit retardée durant quelques cycles pour cela.
Les processeurs détectent les dépendances de registre dans un circuit spécialisé, appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée. Si une instruction a une dépendance bloquante, elle est bloquée dans l'étage d'émission en attendant de pouvoir s'exécuter. Durant ce temps d'attente, on insère des vides après l'unité d'émission : certains étages seront inoccupés et n'auront rien à faire. Les étages qui sont situés avant l'étage d'émission sont eux bloqués, la progression des instructions dedans est stoppée.
[[File:Pipeline bubble.png|centre|vignette|upright=2|Pipeline bubble]]
Le terme "émission" reviendra souvent dans la suite du cours. On dira qu'une instruction est émise si elle est envoyée au chemin de donnée, qu'elle quitte l'unité d'émission. Une instruction peut être émise si elle n'a pas de dépendance bloquante, qu'elle peut s'exécuter. Elle rentre alors dans le chemin de donnée, lit ses opérandes et s'exécute. L'unité d'émission est donc la toute fin de l'unité de contrôle, du séquenceur, c'est le point d'entrée du chemin de données.
Dans la suite du cours, nous verrons de nombreux cas dans lesquels bloquer le pipeline est une obligation si on veut que le processeur fonctionne correctement. Il s'agit là d'une fonctionnalité très importante de tous les pipelines, anciens comme modernes. Évidemment, bloquer le pipeline est une perte de performances : le pipeline ne fait rien tant qu'une instruction n'est pas terminée, ce qui va à l'encontre du principe du pipeline. Et de nombreuses techniques d'optimisations visent à réduire les blocages de pipeline, le plus possible. D'ailleurs, nous passerons les prochains chapitres à parler de ces optimisations.
===La détection des dépendances de données===
Le processeur doit détecter les dépendances de données entre instructions. Les dépendances de registres se détectent en comparant entre eux les registres utilisés par les instructions. Les registres étant connus dès la sortie de l'étape de décodage, la détection des dépendances de données se fait de manière précoce, avant même l'exécution des instructions. Une sacré différence avec les dépendances d'adresse, qui demandent que les adresses soient calculées et lues dans les registres avant qu'on puisse détecter les dépendances.
La détection des dépendances de registre se fait dans l'étage d'émission, situé juste après l'étage de décodage d'instruction. L'unité d'émission reçoit une instruction décodée et vérifie ses dépendances avec les instructions dans le pipeline. À chaque cycle d'horloge, l'unité d'émission analyse une instruction décodée appelée '''instruction candidate'''. Elle détecte si cette instruction candidate a une dépendance avec une instruction déjà dans le pipeline. Les instructions dans le pipeline n'ont pas encore enregistrées leurs résultats dans les registres, soit parce qu'elles sont en cours d'exécution, qu'elles lisent leurs opérandes, peut importe. Elles seront appelées '''instructions "en vol"'''.
Pour cela, l'unité d'émission compare les registres utilisés par l'instruction candidate et les instructions dans le pipeline. Il n'y a pas de dépendance si ces registres sont différents, alors qu'il y a dépendance dans le cas contraire. Pour être plus précis, il faut cependant faire une différence entre les registres opérandes et de destination. Les '''registres opérandes''' sont ceux qui sont lus, ceux où sont les opérandes. Le '''registre de destination''' d'une instruction est celui dans lequel on enregistre le résultat. Les quatre situations possibles sont les suivantes :
* Un des registres opérande est à lire (opérande) par une instruction dans le pipeline : pas de dépendance.
* Un des registres opérande est écrit (destination) par une instruction dans le pipeline : dépendance RAW.
* Un des registres de destination est à lire (opérande) par une instruction dans le pipeline : potentielle dépendance WAR, mais seulement sur des pipelines pathologiques jamais utilisés.
* Un des registres de destination est écrit (destination) par une instruction dans le pipeline : dépendance WAW.
Notez que pour les lectures, on part du principe que les instructions en vol n'ont pas encore lu leurs registres opérandes. Dès qu'elles ont lu les opérandes en question, les dépendances liées disparaissent. Pareil pour les écritures, sauf que celles-ci ont lieu à la toute fin du pipeline. Les écritures en attente ont donc lieu quand l'instruction termine.
{|class="wikitable"
|-
! rowspan="2" colspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! rowspan="2" | Instructions en vol
! Registres pas encore lus (opérandes)
| Pas de dépendances (dépendance RAR inoffensive)
| Dépendance WAR potentielle, pas en pratique
|-
! Registre pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
L'algorithme est donc le suivant : une lecture est autorisée en absence d'écriture (dans le même registre), une écriture est autorisée en absence de lecture (dans le même registre). Cependant, on peut grandement simplifier l'algorithme et ne pas tenir compte des dépendances WAR, qui n'existent pas sur la plupart des pipelines dynamiques sans exécution dans le désordre. Le tableau se simplifie en retirant les dépendances inutiles, seule la dernière ligne nous intéresse au final.
{|class="wikitable"
|-
! rowspan="2" colspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! Instructions en vol
! Registre pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
===L'exécution dans l'ordre et dans le désordre des instructions===
Nous venons de voir que les dépendances de données posent des problèmes qu'un pipeline de longueur variable doit résoudre. La solution la plus simple retarde les instructions candidates problématiques de quelques cycles d'horloges. Si une instruction a une dépendance problématique avec une autre, il suffit simplement de l’exécuter avec un retard de cycles. Pour cela, lorsqu'une instruction multicycle est démarrée, les instructions problématiques sont bloquées à l'étage de décodage en attendant le bon moment. Pour cela, on retarde l'instruction fautive dans l'unité de décodage.
[[File:Pipeline bubble.png|centre|vignette|upright=2|Bulle de pipeline.]]
Notons que cette technique exécute des instructions indépendantes dans des unités de calcul séparées. Dans l'exemple précédent avec une instruction multicycle suivie par une instruction normale, le processeur détecte les situations où ce n'est pas grave si la seconde instruction finisse avant la première : on peut alors lancer la seconde instruction immédiatement sans attendre. Il y a donc un gain en performance. Un point important est que cette technique émet les instructions dans l'ordre, mais qu'elles peuvent s'exécuter dans le désordre et terminer dans le désordre. En clair, les instructions sont injectées dans les unités de calcul dans l'ordre, mais en sortent dans le désordre.
Pour éviter de bloquer le pipeline, les processeurs modernes incorporent des optimisations assez intéressantes. Nous verrons la technique du contournement dans le chapitre suivant. Elle permet de ne pas bloquer le pipeline en cas de dépendance ''Read after write''. Une solution complémentaire est ce qu'on appelle l''''exécution dans le désordre''', une optimisation commune sur les processeurs modernes qui sera vue en détail dans plusieurs chapitres dans la suite du cours.
L'idée est que si une instruction problématique est bloquée dans l'unité de décodage/émission, elle est juste mise en attente, elle ne bloque pas le pipeline. Le processeur regarde les instructions suivantes et vérifie si certaines d'entre elles peuvent s'exécuter. Les instructions mises en attente sont exécutées dès que possible, elles ont la priorité. La gestion de la mise en attente des instructions problématiques est assez complexe et implique de les stocker dans des mémoires spécifiques. Là encore, il faut que les instructions exécutées en parallèle ne manipulent pas les mêmes données, ni les mêmes registres, ni les mêmes adresses mémoires.
La toute première implémentation de l'exécution dans le désordre était sur l'ordinateur CDC 6600, suivi par son successeur le CDC 7600. Il intégrait un circuit appelé le '''''scoreboard''''', qui gérait l'exécution dans le désordre. Décrire le circuit en question est assez compliqué, aussi je le laisse de côté pour le moment. Sachez le terme ''scoreboard'' est resté et a été élargit à tout circuit d'émission, qu'il soit dans le désordre ou dans l'ordre.
==L'étage d'émission : le blocage du pipeline==
Maintenant, voyons comment est implémenté l'étage d'émission (''issue''), qui détecte les situations problématiques et bloque le pipeline si besoin. Il suit immédiatement l'étage de décodage, et est situé avant le chemin de données. Le rôle de l'unité d'émission est de générer de bloquer le pipeline. Si elle reçoit une instruction problématique, elle la bloque dans l'unité d'émission et bloque le pipeline tant que la dépendance n'est pas réglée. Mais si l'instruction peut s'exécuter, elle l'envoie dans la suite du pipeline. On dit qu'elle émet l'instruction.
L''''émission''' est donc l'envoi d'une micro-opération dans le pipeline, si les conditions sont réunies. Un point important de l'émission est que le processeur émet une instruction par cycle, en absence de dépendance de données, mais aussi en absence de dépendances structurelles, les deux étant détectées par l'unité d'émission.
===La gestion des dépendances de données===
Pour rappel, l'unité d'émission détecte les dépendances entre une '''instruction candidate''' et les instructions déjà dans le pipeline, aussi appelées '''instructions "en vol"'''. L'unité d'émission détecte les dépendances entre l'instruction candidate et chaque instruction en vol. Pour rappel, les dépendances de données se détectent en comparant les instructions de deux opérandes. Vu que les dépendances WAR sont très rares, et n'existent pas sur les pipelines normaux, on ne tient compte que des dépendances RAW et WAW. Les détecter demande de comparer les registres de l'instruction candidate, avec les registres de destination des instructions en vol.
{|class="wikitable"
|-
! rowspan="2" |
! colspan="2" | Instruction candidate
|-
! Registres opérandes !! Registre destination
|-
! Instructions en vol
Registres pas encore écrits (destination)
| Dépendance RAW
| Dépendance WAW
|}
L'unité d'émission doit connaitre les registres de destination des instructions en vol, ce qui peut se faire de plusieurs manières. La première méthode utilise le fait que les noms de registres sont propagés dans le pipeline, comme tous les signaux de commande. Dans ces conditions, rien n’empêche de relier les registres tampons chargés de la propagation à l'unité d'émission. L'unité d'émission est donc un paquet de comparateurs reliés par des portes OU. En sortie, elle fournit un signal STALL, qui indique s'il faut bloquer le pipeline ou non. Pour un pipeline à N étages maximum, cela demande de faire N comparaisons.
[[File:Détection des dépendances dans un pipeline, sans scoreboard.png|centre|vignette|upright=2|Détection des dépendances dans un pipeline.]]
Une autre implémentation, beaucoup plus économe en circuits, se limite à un simple '''registre de réservation'''. Il permet à une instruction de réserver en écriture son registre de destination. Par réservé, on veut dire que le registre ne peut pas être écrit par une autre instruction. Les registres qui ne sont pas réservés sont dit libres ou disponibles. Si une instruction candidate veut lire ce registre, c'est signe qu'il y a une dépendance RAW : elle veut lire un registre réservé, qui est destiné à être écrit par une instruction en vol. Si elle veut écrire, c'est signe qu'elle veut écrire dans un registre où une autre instruction veut écrire : c'est une dépendance WAW. Dans les deux cas, l'instruction n'est pas émise.
Le registre encode les registres réservés en représentation ''one-hot'' modifiée. Chaque bit est associé à un registre. Le bit numéro 0 est associé au registre numéro 0, le bit numéro 1 au registre numéro 1, etc. Un registre est réservé en mettant le bit associé à 1.
{|class="wikitable"
|+ Registre de réservation
|-
! Registre 7 !! Registre 6 !! Registre 5 !! Registre 4 !! Registre 3 !! Registre 2 !! Registre 1 !! Registre 0
|-
| 0 || 0 || 1 || 0 || 0 || 1 || 0 || 0
|}
Il existe une seconde possibilité, où les registres réservés sont à 0 et les registres libres à 1. Nous l’appellerons le '''registre de disponibilité'''.
Avant d'émettre une instruction, l'unité d'émission extrait les registres opérande/destination et les convertis dans la même représentation que le registre de réservation, à savoir en représentation ''one-hot''. Le circuit de traduction binaire vers ''one-hot'' est, pour rappel, un simple décodeur. Puis, on fait un OU logique entre les sorties des décodeurs. Le résultat est un masque qui indique quels registres sont lus par l'instruction, encodé en représentation ''one-hot''. Appelons-le le '''masque d'opérandes'''. Le masque est alors comparé au registre de réservation pour vérifier si l'instruction peut être émise.
[[File:Unité d'émission simple, dans l'ordre.png|centre|vignette|upright=2|Unité d'émission simple, dans l'ordre]]
Si l'émission est autorisée, le registre de réservation est là aussi mis à jour. La mise à jour du registre de réservation à l'émission est simple : on prend le registre de destination, encodé en ''one-hot'', et on le combine avec le registre de réservation, en faisant un simple OU logique. Le registre de réservationest aussi mis à jour dès qu'une instruction enregistre son résultat dans les registres, ou alors dès qu'il est disponible pour le contournement. Le bit associé au registre repasse alors à 0 (ou à 1). Sans contournement, la mise à jour se fait alors à la toute fin de l'instruction, quand elle se termine, lors de la dernière étape d'enregistrement, à la fin de son pipeline. Avec, elle se fait quand l'instruction quitte l'unité de calcul.
Vous aurez peut-être remarqué, mais une différence est que les pipelines fixes retardent les écritures dans le banc de registre avec des registres d'échelonnage, les pipelines dynamique retardent l'émission des instructions. Les deux méthodes ont sensiblement le même résultat.
Le '''registre de consultation''' permet de désigner les registres qui vont être lus par les instructions en vol. Je précise bien : les registres qui n'ont pas encore été lus. Les registres en question sont dit consulté, les autres sont dit non-consultés. Il est interdit d'écrire dans un registre consulté, c'est signe d'une dépendance WAR. Il faut noter que beaucoup de pipeline s'en passent car ils n'ont pas de dépendances WAR. Le registre de consultation est mis à jour dès que l'opérande est lue. Et cela arrive assez tôt dans le pipeline, au bout de quelques cycles grand maximum. Sur beaucoup de pipelines, il n'est pas présent dans il est impossible qu'une écriture ait lieu avant qu'une lecture ait démarrée. Mais sur les pipelines où de telles dépendances sont possibles, il faut en tenir compte.
Il existe quelques rares processeurs qui fusionnent le registre de réservation et de consultation en un seul. Le registre contient autant de bits que de registres, chaque bit indique si le registre associé est pas encore lu/écrit. De tels implémentations détectent alors toutes les dépendances, même les dépendances ''Read After Read''. Elles tendent à être très conservatrices. Un exemple est le co-processeur vectoriel VFP9-S d'ARM. Leur implémentation est simple, mais il peut y avoir un cout en performance.
===La gestion de la disponibilité des ALUs===
L'étage d'émission détecte aussi les dépendances structurelles au niveau de l'unité de calcul. Elle apparaissent quand deux instructions veulent exécuter une instruction en même temps, sur la même ALU. Elles impliquent généralement des instructions multicycles, qui occupent une unité de calcul pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Face à une dépendance structurelle de ce type, l'unité d'émission met la seconde instruction en attente, tant que l'unité de calcul est occupée. L'unité d'émission bloque le pipeline si le processeur utilise l'émission dans l'ordre. Reste à détecter les conflits d'accès, les dépendances structurelles.
Une solution consiste à déterminer les paires d'instructions consécutives qui peuvent donner lieu à des dépendances structurelles. Par exemple, si une instruction de multiplication est dans le pipeline, je sais que je ne peux pas démarrer de nouvelle multiplication immédiatement après, s'il n'y a qu'un seul multiplieur. Détecter les dépendances structurelles consiste à comparer l'opcode de l'instruction à émettre, avec les opcodes des instructions en cours d’exécution dans le pipeline. Si jamais l'opcode de l'instruction à émettre et une opcode dans le pipeline forment une paire d'opérations interdite, il y a dépendance structurelle et on doit bloquer le pipeline.
Une autre solution mémorise un bit pour chaque ALU qui indique si elle est occupée. L'ensemble des bits est appelé le '''vecteur de disponibilité des ALUs'''.
{|class="wikitable"
|+ Vecteur de disponibilité des ALUs
|-
! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY
|-
| 0 || 0 || 1 || 0 || 0
|}
Avant d'émettre une instruction, l'unité d'émission vérifie si une unité de calcul adéquate est disponible. Pour cela, chaque instruction est associée à un masque qui précise quelles unité de calcul peuvent l'exécuter. Détecter une dépendance structurelle demande une simple opération de masquage entre ce masque et le vecteur de disponibilité des ALUs.
{|class="wikitable"
|+ Masque par instruction, associé au vecteur de disponibilité précédent
|-
! !! ADD1 BUSY !! ADD2 BUSY !! MUL BUSY !! COMP CUSY !! LOAD /STORE BUSY
|-
! ADD
| 1 || 1 || 0 || 0 || 0
|-
! MUL
| 0 || 0 || 1 || 0 || 0
|-
! COMP
| 0 || 0 || 0 || 1 || 0
|-
! LOAD
| 0 || 0 || 0 || 0 || 1
|-
! STORE
| 0 || 0 || 0 || 0 || 1
|-
|}
Toute la difficulté tient dans la mise à jour des bits de disponibilité des ALUs. Une solution assez simple intègre des compteurs dans le processeur pour chaque bit de disponibilité. La technique part du principe que la durée de chaque instruction est connue et qu'on peut l'utiliser pour initialiser un compteur. Lors de l'émission d'une instruction sur une ALU, le compteur associé à l'ALU est initialisé avec le nombre de cycles de l'instruction. Il est décrémenté à chaque cycle d'horloge. Une fois qu'il atteint 0, le bit de disponibilité est mis à 0.
Une autre solution délègue cette tâche à l'unité de calcul. Dès qu'un calcul se termine sur l'ALU, l'unité de calcul prévient qu'elle est libre avec un signal FREE qu'elle envoie à l'unité d'émission. L'avantage est que la solution prend en charge naturellement le cas où les opérations sont de durée variable. Un exemple est celui d'un additionneur à saut de retenue, où la durée d'une addition dépend de la propagation de la retenue. Un autre exemple est celui de certains multiplieurs itératifs (qui font la multiplication en effectuant une suite de décalages et d'addition), qui fournissent le résultat en avance pour certaines opérandes. Ou encore les circuits diviseurs par décalage/soustraction qui s’arrêtent dès que le dividende partiel tombe à zéro. Et d'ailleurs, on va voit que cette solution marche assez bien pour les instructions mémoire.
==L'émission et les micro-opérations mémoire==
Les micro-opérations mémoires sont gérées par l''''unité mémoire''', ou encore la ''Load-Store Unit''. Nous utiliserons le terme unité mémoire dans la suite du cours. L'unité mémoire est pour le moment une sorte de boite noire, dont on ne détaillera pas l'intérieur, et dont seules les entrées et sorties nous sont accessibles. L'unité d'émission la considère d'ailleurs comme telle. L'unité mémoire prend en entrée des opérandes, à savoir les adresses à lire/écrire, éventuellement la donnée à écrire. En sortie, elle fournit la donnée lue et le registre de destination dans lequel enregistrer la donnée. Les entrées et sorties sont reliées au réseau de contournement, ainsi qu'aux registres.
En général, l'adresse est lue dans les registres, mais elle peut aussi être fournie par le réseau de contournement. Mais certains modes d'adressage font que l'adresse doit être calculée. L'adressage base + indice est un bon exemple : il demande de calculer l'adresse à lire/écrire avec une addition et éventuellement un décalage. Dans le chapitre sur le pipeline, nous avons vu que les calculs d'adresse peuvent être effectués soit dans les unités de calcul entières, soit dans l'unité mémoire elle-même. Et les deux solutions ne sont pas équivalentes, elles ont chacune leurs avantages et leurs défauts. Un des avantages est lié au décodage des instructions, à savoir si les instructions mémoire sont décodées en une ou deux micro-opérations.
La solution la plus simple est celle qui effectue les calculs d'adresse dans l'unité mémoire elle-même. L'interface de l'unité mémoire est modifiée, de manière à pouvoir lui envoyer les indices et décalages nécessaires au calcul d'adresse. Si l'unité mémoire effectue les calculs d'adresse, les instructions mémoire sont décodées en une seule micro-opération, qui est envoyée à l'unité mémoire. Pas besoin de micro-opération séparée pour le calcul d'adresse. Le défaut de cette méthode est que l'unité mémoire doit incorporer une unité de calcul d'adresse, ce qui ajoute quelques circuits redondants avec l'ALU entière. Mais les circuits ajoutés se résument à des additionneurs et un circuit de décalage, ce qui est très léger. De nos jours, tous les processeurs font les calculs d'adresse dans l'unité mémoire. Le fait que le décodage soit simplifié l'emporte haut la main, comparé aux économies liées à l'unité de calcul d'adresse.
[[File:Unité mémoire dans un pipeline.png|centre|vignette|upright=2.5|Unité mémoire avec calcul d'adresse intégré, dans un pipeline.]]
Vous vous dites sans doute que les accès mémoire sont des instructions multicycles, et ce n'est pas faux. Mais elles se distinguent des autres instructions multicycle par leur temps d'exécution variable. Un accès au cache peut prendre deux à trois cycles d’horloge, un accès au cache L2 prend facilement 10 cycles, et un accès en RAM une centaine. Vu que leur latence n'est pas connue à l'avance, les techniques utilisées sur les pipelines multicycle/dynamiques ne fonctionnent pas parfaitement. La gestion des dépendances de données est notamment totalement différente.
Gérer les accès mémoire demande, entre autres, de gérer la disponibilité de l'unité mémoire. Mais contrairement aux instructions de calcul, les accès mémoire ont pour défaut qu'ils ont un nombre de cycles variable. Sur la plupart des processeurs, l'accès au cache se fait rapidement, mais l'accès à la mémoire est très lent. Et l'unité d'émission doit gérer la disponibilité de l'unité mémoire.
===Les unités mémoires bloquantes===
Dans ce qui suit, nous partons du principe que l'unité mémoire ne peut faire qu'un seul accès mémoire à la fois. Hypothèse crédible, mais certains processeurs avec exécution dans le désordre fonctionnent autrement. L'unité mémoire prévient elle-même de sa disponibilité. Elle est reliée à l'unité d'émission via un fil MEM_UNIT_READY qui prévient qu'elle est disponible et peut accepter un nouvel accès mémoire. Si le bit MEM_UNIT_READY est à 1, l'unité mémoire est inutilisé, il n'y a pas d'accès mémoire en cours. Par contre, si ce bit est à 0, alors un accès mémoire est en cours et l'unité mémoire ne peut pas accepter de nouvel accès mémoire.
L'unité d'émission tient en compte de ce bit pour décider de l'émission des instructions en cours. De plus, elle l'utilise pour déterminer si le registre de destination est disponible. Quand le signal MEM_UNIT_READY passe de 0 à 1, le no/numéro du registre destination est disponible en sortie de l'unité mémoire. Il est alors envoyé à l'unité d'émission et le registre de disponibilité est mis à jour.
[[File:Implémentation du stall lors d'un accès mémoire dans l'unité d'émission.png|centre|vignette|upright=2|Implémentation du stall lors d'un accès mémoire dans l'unité d'émission]]
Pour éviter tout problème, l'idéal est de stopper l'exécution d'autres instructions tant qu'un accès mémoire a lieu. Les instructions qui suivent l'accès mémoire sont mises en attente tant qu'il n'est pas terminé. En clair, on bloque le pipeline, l'unité d'émission cesse d'émettre des instructions et bloque le pipeline. Pour cela, dès que l'unité d'émission émet une micro-opération mémoire, elle bloque le pipeline tant que le signal MEM_UNIT_READY est à 0. Une fois l'accès mémoire terminé, l'unité d'accès mémoire met le signal MEM_UNIT_READY à 1, ce qui prévient l'unité d'émission. L'émission des instructions reprend. C'est la solution la plus simple, mais elle est imparfaite.
===Les lectures non-bloquantes===
Les '''lectures non-bloquante''' sont une optimisation qui évite de bloquer le pipeline en cas d'accès mémoire. Elle a été utilisée sur le processeur ARM Cortex A53 et sur son successeur, l'ARM Cortex A510, avec un certain succès. L'idée est de continuer à faire des calculs en parallèle de l'accès mémoire, sous condition que cela ne pose pas de problèmes. Les instructions exécutées en parallèle sont exécutées dans l'unité de calcul, nous allons donc les appeler des opérations. L'unité d'émission ne peut pas émettre de nouvel accès mémoire, mais peut exécuter des opérations en parallèle de la lecture, si elles sont indépendantes de la lecture. Reste à savoir sous quelles conditions les opérations sont indépendants de la lecture.
La technique des lectures non-bloquantes ne se préoccupe pas des dépendances d'adresses. En effet, elles sont gérées par l'unité mémoire, pas par l'unité d'émission. Elle ne se préoccupe que des dépendances de registre. Les lectures et écritures lisent des opérandes dans les registres et ne sont pas émises tant qu'il y a une dépendance avec ces registres. Mais ce qui nous intéresse est ce qui se passe une fois la lecture/écriture émise. Une fois émises, les écritures n'écrivent dans aucun registre et n'ont donc pas de dépendances de registre. Par contre, les lectures écrivent la donnée lue dans un registre de destination, ce qui peut faire apparaitre des dépendances.
Le premier problème est qu'il se peut qu'une opération utilise la donnée lue par l'accès mémoire. Par exemple, la lecture charge une donnée dans un registre et ce registre est ensuite utilisé comme opérande d'une addition. Dans ce cas, l'opération ne doit pas s’exécuter tant que la donnée n'a pas été chargée. Un autre cas est celui où une opération écrit dans le registre de destination de la lecture. Avec des lectures non-bloquantes, l'opération ne doit pas démarrer tant que la lecture n'est pas terminée. Les dépendances pertinentes ici sont des dépendances avec le registre de destination de la lecture.
L'unité d'émission vérifie les dépendances avec le registre de destination de la lecture. Pour cela, l'unité d'émission marque le registre de destination de la lecture comme « réservé », du moins tant que la lecture n'a pas écrit son résultat dedans. Lorsqu'elle émet une lecture, l'unité d'émission marque le registre de destination comme réservé. Elle continue d’émettre des instructions si elles sont indépendantes de ce registre destination, mais bloque l'émission de toute instruction qui l'utilise. Ainsi, les instructions qui ne lisent ou n'écrivent pas ce registre peuvent s'exécuter dans les unités de calcul.
L'implémentation de cette technique demande juste de pouvoir réserver des registres, et consulter les réservations à chaque cycle. Pour marquer les registres comme réservés/non-réservé, l'unité d'émission contient un ''bit de réservation en lecture'' par registre. Chaque bit indique si le registre associé contient une donnée valide ou non. Le bit de validité est automatiquement comme invalide quand le processeur démarre une lecture. Le tout est regroupé dans un '''registre de réservation'''. Le registre de réservation peut être fusionné avec le registre de disponibilité vu plus haut.
Toute instruction qui a un registre invalide comme opérande est mise en attente. Idem pour les instructions qui ont un registre invalide comme registre de destination. À chaque cycle, l'unité d'émission tente de démarrer une nouvelle instruction et vérifie si elle est dépendante de la lecture. Elle vérifie si l'instruction à émettre a le registre de destination de la lecture comme opérande, ou comme registre de destination. Si c'est le cas, alors l'instruction est bloquée dans le pipeline. Mais dans le cas contraire, l'instruction de calcul est indépendante de l'accès mémoire, et peuvent démarrer sans trop de problèmes.
===L'exécution dans le désordre et autres alternatives===
Les lectures non-bloquantes peuvent être vues comme une sorte d'exécution dans le désordre limitée aux lectures. La différence avec l'exécution dans le désordre est que la technique des lectures non-bloquantes détecte si une instruction est dépendante d'une lecture, alors que l’exécution dans le désordre détectent toutes les dépendances entre instructions, pas seulement les dépendances avec un accès mémoire.
Le processeur ROCK, annulé en 2010, utilisait une version améliorée des lectures non-bloquantes, appelée la technique des '''éclaireurs matériels'''. La différence est que le processeur ne stoppe pas quand il rencontre une instruction dépendante de la lecture. À la place, les instructions dépendantes de la lecture sont mises en attente dans une file d'attente spécialisée, appelée la '''file d’attente différée''' (''deferred queue''), qui est une mémoire FIFO un peu modifiée. Une fois que la lecture renvoie un résultat, le processeur cesse l’exécution des instructions et exécute les instructions mises en attente. Les instructions mises en attente sont exécutées dans l'ordre, avant de reprendre là où le programme s'était arrêté.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les pipelines multicycles
| prevText=Les pipelines multicycles
| next=Le contournement (data forwarding)
| nextText=Le contournement (data forwarding)
}}
</noinclude>
sxggz1v1dis6usr1p9uqbha1nik9ek4
Fonctionnement d'un ordinateur/Les pipelines dynamiques
0
65886
774562
771981
2026-10-09T20:47:43Z
Mewtow
31375
774562
wikitext
text/x-wiki
La quasi-totalité des processeurs, modernes comme anciens, supportent des '''instructions multi-cycle''', à savoir des instructions qui mettent plusieurs cycles à s'exécuter. Un exemple typique est celui des opérations flottantes ou des multiplications. Et gérer de telles instructions avec un pipeline pose de nombreux problèmes d'implémentation. Les instructions multi-cycles ne semblent pas vraiment coller avec un pipeline, qui a une longueur fixe, pour ce qu'on en sait pour le moment.
Pour le moment, nous avons vu des pipelines assez simples, comme le pipeline Fetch-Decode-Exec ou le pipeline RISC classique à 5 étages. De tels pipelines ont un nombre d'étages fixe, qui est le même pour toutes les instructions. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''.
De rares processeurs se privaient d'instructions multicycles, ils ne supportaient pas la multiplication ni les opérations flottantes. Les tout premiers processeurs MIPS étaient dans ce cas, mais seulement les tout premiers. Les processeurs MIPS ont rapidement supporté les instructions multicycles. Par contre, la présence d'instructions multicycles complexifie grandement le pipeline. Dans ce chapitre, nous allons voir comment modifier un processeur à pipeline, pour qu'il gère des instructions multi-cycles. La modification donne un '''pipeline multi-cycle''', aussi appelé un '''pipeline dynamique'''. Nous utiliserons les deux termes dans ce cours.
Avec un pipeline multicycle, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. L'implémentation d'un pipeline multicycle est assez simple : il y a juste besoin d'utiliser des unités de calcul dédiées pour les instructions lentes, avec par exemple un circuit multiplieur séparé d'une ALU entière. Le temps de calcul dépend de l'ALU, et donc le nombre de cycles de l'instruction associée.
[[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]]
==Les pipelines multi-cycles séparent ''front-end'' et ''back-end''==
Avec un pipeline multicycle, le processeur est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat.
[[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec un aval et un amont (back-end et front-end).]]
===La spécialisation des avals===
La quasi-totalité des processeurs modernes dispose d'une voie séparée pour les accès mémoire, une '''unité mémoire''' dédiée. L'unité mémoire est séparée pour une bonne raison : les accès mémoire se marient assez mal avec un pipeline. Les accès mémoire ont une latence élevée, variable. Ils sont difficiles à pipeliner de manière générale. La voie pour les accès mémoire sert donc d'exception, c'est une unité qui n'est pas tout à fait pipelinée, alors que le reste du chemin de données l'est. Sa connexion avec le pipeline varie grandement suivant le processeur.
[[File:Pipeline à deux aval de type load-store.png|centre|vignette|upright=1.5|Pipeline à deux aval de type load-store.]]
Si on omet les voies liées aux accès mémoire, les autres voies correspondent peu ou prou à une unité de calcul. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée.
La destination de chaque voie peut varier. Les instructions de calcul ou les lectures enregistrent un résultat dans les registres, et se terminent donc sur le port d'écriture du banc de registres. Par contre, les voies pour les branchements ou les écritures n'enregistrent rien dans les registres. La voie pour les branchements se termine dans le séquenceur et/ou dans le registre d'état, la voie pour les écritures ne finit nulle part si ce n'est dans l'unité mémoire.
[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|centre|vignette|upright=1.5|Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements]]
Plus tard dans la suite du cours, nous verrons que les processeurs modernes regroupent plusieurs ALUs par voie, pour des raisons assez compliquées à expliquer ici. Retenez juste que le chemin de données est composé de plusieurs voies relativement indépendantes, chacune spécialisée dans un type de micro-opération spécialisé. Les voies commencent toutes avec la lecture des opérandes, elles effectuent un calcul, puis enregistrent leurs résultats dans les registres si besoin.
===Les processeurs à émission simple et multiple===
Toujours est-il que le décodeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie.
Sur les processeurs dits à '''simple émission''', une instruction est envoyée dans une voie à chaque cycle, soit dans une ALU, soit dans l'unité mémoire, soit dans les voies pour les branchements, etc. Ils peuvent émettre au maximum une instruction apr cycle d'horloge, ils exécutent en moyenne une instruction par cycle. Diverses optimisations comme le renommage de registres et l'exécution dans le désordre permettent de se rapprocher de ce rythme de croisière d'une instruction lancée par cycle.
Mais il existe des processeurs à '''émission multiple''' qui sont capables d'émettre plusieurs instructions à la fois, chacune dans des voies séparées. Les processeurs dits superscalaires sont dans ce cas, mais ils ne sont pas les seuls. Ils permettent de gagner en performance en utilisant au maximum les unités de calcul/voies. Si plusieurs unités de calcul sont inoccupées, un processeur à émission multiple pourra lancer une instruction dans chacune d'entre elles en même temps, ce qui permet de les occuper le plus vite possible. De moins, c'est le cas si les conditions adéquates sont réunies, mais laissons cela pour plus tard. Un chapitre entier sera dédié aux processeurs à émission multiple.
==Les dépendances structurelles : l'occupation de chaque voie==
Les pipelines multi-cycles doivent gérer des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Les dépendances structurelles sont généralement assez rares, car les pipelines prennent bien garde à bien séparer les étages du pipeline, en dupliquant des circuits s'il le faut. Par exemple, séparer le cache en un cache d'instruction et un cache de donnée élimine une dépendance structurelle. Mais sur les pipeline multi-cycles, c'est une autre histoire. Mais pour comprendre pourquoi, il faut voir quelles sont les sources de ces dépendances. Pour résumer, elles se situent au niveau des unités de calcul et du banc de registre.
===Les dépendances structurelles liées aux unités de calcul===
En théorie, une fois qu'une instruction est décodée, elle est envoyée dans la voie adaptée. Du moins, si cette voie est libre. En effet, l'usage d'instructions multicycles fait qu'une voie peut être occupée pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Et en général, des instructions consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs.
En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique.
La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle.
[[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]]
Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant.
===Les dépendances structurelles à l'écriture dans le banc de registre===
Les instructions multi-cycle lisent et écrivent dans le même banc de registres que les instructions 1-cycle. Le résultat est qu'elles peuvent écrire dans les registres en même temps. Par exemple, imaginons que l'on exécute une multiplication de 6 cycles, suivie au cycle suivant par une opération qui n'en prend que 5. Elles tenteront d'enregistrer leurs données en même temps 6 cycles après pour la première, 1 + 5 cycles pour l'autre. Elles voudront donc enregistre une donnée en même temps. Mais le banc de registre n'a qu'un seul port d'écriture...
La solution la plus simple est que la seconde instruction doit être retardée d'un cycle pour résoudre le problème. En soi, rien de problématique à gérer, l'unité de décodage a juste à insérer des bulles de pipeline. Une autre solution ajoute des ports d'écriture sur le banc de registres, afin que les deux instructions peuvent écrire en même temps dans le banc de registre. Il faut juste qu'elles écrivent dans des registres différents, mais il s'agit d'un cas de dépendance de données, pas une dépendance structurelle proprement dit.
La première solution a un cout en performance, mais est simple à implémenter. Son cout en transistor est limité, tout est concentré dans l'unité de décodage/émission. Pour la seconde solution, son cout en transistor est important, sans compter que le banc de registre devient alors plus gourmand en énergie. Le tout est à comparer à un gain en performance généralement limité. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul.
Les premiers processeurs MIPS avaient une technique très particulière pour éliminer les dépendances structurelles de ce genre, qui éliminait aussi partiellement les dépendances WAW. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications et éventuellement les divisions. Ils ajoutaient un aval séparé, un simple circuit multiplieur/diviseur séparé de l'ALU entière. Les instructions multi-cycles disposaient de leur propre set de registres rien que pour elles, ce qui résolvait pas mal de problèmes de conflits d'accès aux registres.
==L'inversion de l'ordre des écritures==
Un problème des pipelines dynamiques est qu'ils ne garantissent pas l'ordre des écritures. Si on émet des instructions dans l'ordre du programme, il se peut qu'elles ne terminent pas dans le même ordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé. Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre si elles n'ont pas de dépendances entre elles. On dit qu'il y a '''inversion de l'ordre des écritures'''.
[[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]]
L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle...
Au-delà des problèmes liés aux exceptions précises, les dépendances de données posent aussi problème. Si deux écritures se font dans des registres différents, peu importe l'ordre des écritures, tout se passera bien. Mais si les deux écritures se font dans le même registre, il y a un problème : le résultat est incorrect, un registre a été écrasé avec une valeur antérieure. Le problème en question correspond à l'apparition de '''dépendances de donnée de type WAW'''. Les dépendances WAW imposent un ordre concernant les écritures dans les registres et posent des problèmes sur les pipelines dynamiques, uniquement eux.
===Les registres d'échelonnage===
Pour conserver l'ordre des écritures, la solution la plus simple est de '''retarder les écritures''' jusqu'à ce qu'elles atteignent la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. L'implémentation est plus simple si on suppose que toutes les instructions font le même nombre de cycles. On parle alors d'un pipeline de longueur fixe. Le cout à payer est que les instructions doivent se caler sur l'instruction la plus lente. Si une addition met un cycle à s'exécuter et une multiplication 3, alors toutes les instructions font 3 cycles.
Si l'addition fournit son résultat en un cycle, il sera enregistré dans les registres avec un retard de deux cycles. Idem pour les lectures mémoire : si un accès mémoire fait 2 cycles, l'écriture de la donnée lue dans les registres sera retardée d'un cycle. Le retard dépend de l’opération à effectuer, en fonction de combien de cycles elle prend dans l'ALU. En clair, toutes les instructions ont le même nombre d'étages, mais certains étages sont inutiles pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire.
[[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multicycles]]
Reste à ajouter des cycles de retard, qui servent juste à retarder l'enregistrement dans les registres. Pour cela, on insère des registres entre la sortie de l'ALu et le banc de registre. Les registres en question sont appelés des ''staggering registers'', ou encore des '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe.
[[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]]
Un défaut de la méthode précédente est que les données sont copiées d'un registre à l'autre à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé.
[[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]]
Il faut rappeler que pour les écritures en RAM, le processeur utilise une file d'écriture pour garantir que les écritures se font dans l'ordre. Nous en avions parlé dans le chapitre sur le pipeline, quand nous avions parlé des exceptions précises. On peut la voir comme une sorte d'équivalent des registres d'échelonnage, mais pour les écritures en mémoire. La file d'écriture est séparée des registres d'échelonnage, pour une raison simple : elle ne communique pas avec les registres, mais avec le cache. Sa sortie étant différente, elle est donc séparée.
===Le tampon de réordonnancement===
Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit.
La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe.
[[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]]
Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats.
Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc.
[[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]]
Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée.
Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises.
{|class="wikitable"
|+ Une entrée du ROB
|-
| ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception
|}
Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ
Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans.
Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement.
===Le tampon d’historique===
Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB).
Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception.
[[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]]
===Le banc de registres futurs===
Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc.
Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit.
Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct.
Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.*
[[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]]
Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres.
===Les point de contrôle de registre===
La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements.
Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur.
Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde.
[[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]]
Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc.
La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF.
===La complétion dans le désordre===
Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''.
En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté.
Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il s’applique en absence de branchements et peut être vu comme une amélioration des lectures non-bloquantes. Le mécanisme s'active quand une lecture est bloquée par un défaut de cache ou toute autre situation. Le processeur implémente l'exécution dans le désordre, ce qui fait qu'il exécute les instructions qui suivent une lecture bloquée, à condition qu'elles ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en attente. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon.
Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU.
==La fréquence des avals des pipelines multicycle==
L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations.
===Le ''clock gating'' des avals inutilisés===
Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''.
Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache.
Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge.
La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie.
===Les unités de calcul à double fréquence du Pentium 4===
Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe.
L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel.
Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles.
La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les optimisations du chargement des instructions
| prevText=Les optimisations du chargement des instructions
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
9y3po9wbz7fu1c543kymn7q0edaav5q
774580
774562
2026-10-10T11:38:14Z
Mewtow
31375
774580
wikitext
text/x-wiki
La quasi-totalité des processeurs, modernes comme anciens, supportent des '''instructions multi-cycle''', à savoir des instructions qui mettent plusieurs cycles à s'exécuter. Un exemple typique est celui des opérations flottantes ou des multiplications. Et gérer de telles instructions avec un pipeline pose de nombreux problèmes d'implémentation. Les instructions multi-cycles ne semblent pas vraiment coller avec un pipeline, qui a une longueur fixe, pour ce qu'on en sait pour le moment.
Pour le moment, nous avons vu des pipelines assez simples, comme le pipeline Fetch-Decode-Exec ou le pipeline RISC classique à 5 étages. De tels pipelines ont un nombre d'étages fixe, qui est le même pour toutes les instructions. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Un pipeline fixe peut supporter les instructions multicycles, mais cela demande d'utiliser des blocages de pipeline. Une instruction multicycle occupe l'étage d’exécution pendant plusieurs cycles, les étages précédents sont gelés pendant ce temps.
Dans ce chapitre, nous allons voir comment modifier un processeur à pipeline, pour qu'il gère des instructions multi-cycles sans utiliser de blocages. La modification donne un '''pipeline dynamique'''. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. L'implémentation d'un pipeline dynamique est assez simple : il y a juste besoin d'utiliser des unités de calcul dédiées pour les instructions lentes, avec par exemple un circuit multiplieur séparé d'une ALU entière. Le temps de calcul dépend de l'ALU, et donc le nombre de cycles de l'instruction associée.
[[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]]
==Les pipelines multi-cycles séparent ''front-end'' et ''back-end''==
Avec un pipeline dynamique, le processeur est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat.
[[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec un aval et un amont (back-end et front-end).]]
===La spécialisation des avals===
La quasi-totalité des processeurs modernes dispose d'une voie séparée pour les accès mémoire, une '''unité mémoire''' dédiée. L'unité mémoire est séparée pour une bonne raison : les accès mémoire se marient assez mal avec un pipeline. Les accès mémoire ont une latence élevée, variable. Ils sont difficiles à pipeliner de manière générale. La voie pour les accès mémoire sert donc d'exception, c'est une unité qui n'est pas tout à fait pipelinée, alors que le reste du chemin de données l'est. Sa connexion avec le pipeline varie grandement suivant le processeur.
[[File:Pipeline à deux aval de type load-store.png|centre|vignette|upright=1.5|Pipeline à deux aval de type load-store.]]
Si on omet les voies liées aux accès mémoire, les autres voies correspondent peu ou prou à une unité de calcul. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée.
La destination de chaque voie peut varier. Les instructions de calcul ou les lectures enregistrent un résultat dans les registres, et se terminent donc sur le port d'écriture du banc de registres. Par contre, les voies pour les branchements ou les écritures n'enregistrent rien dans les registres. La voie pour les branchements se termine dans le séquenceur et/ou dans le registre d'état, la voie pour les écritures ne finit nulle part si ce n'est dans l'unité mémoire.
[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|centre|vignette|upright=1.5|Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements]]
Plus tard dans la suite du cours, nous verrons que les processeurs modernes regroupent plusieurs ALUs par voie, pour des raisons assez compliquées à expliquer ici. Retenez juste que le chemin de données est composé de plusieurs voies relativement indépendantes, chacune spécialisée dans un type de micro-opération spécialisé. Les voies commencent toutes avec la lecture des opérandes, elles effectuent un calcul, puis enregistrent leurs résultats dans les registres si besoin.
===Les processeurs à émission simple et multiple===
Toujours est-il que le décodeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie.
Sur les processeurs dits à '''simple émission''', une instruction est envoyée dans une voie à chaque cycle, soit dans une ALU, soit dans l'unité mémoire, soit dans les voies pour les branchements, etc. Ils peuvent émettre au maximum une instruction apr cycle d'horloge, ils exécutent en moyenne une instruction par cycle. Diverses optimisations comme le renommage de registres et l'exécution dans le désordre permettent de se rapprocher de ce rythme de croisière d'une instruction lancée par cycle.
Mais il existe des processeurs à '''émission multiple''' qui sont capables d'émettre plusieurs instructions à la fois, chacune dans des voies séparées. Les processeurs dits superscalaires sont dans ce cas, mais ils ne sont pas les seuls. Ils permettent de gagner en performance en utilisant au maximum les unités de calcul/voies. Si plusieurs unités de calcul sont inoccupées, un processeur à émission multiple pourra lancer une instruction dans chacune d'entre elles en même temps, ce qui permet de les occuper le plus vite possible. De moins, c'est le cas si les conditions adéquates sont réunies, mais laissons cela pour plus tard. Un chapitre entier sera dédié aux processeurs à émission multiple.
==Les dépendances structurelles : l'occupation de chaque voie==
Les pipelines multi-cycles doivent gérer des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Les dépendances structurelles sont généralement assez rares, car les pipelines prennent bien garde à bien séparer les étages du pipeline, en dupliquant des circuits s'il le faut. Par exemple, séparer le cache en un cache d'instruction et un cache de donnée élimine une dépendance structurelle. Mais sur les pipelines dynamiques, c'est une autre histoire. Mais pour comprendre pourquoi, il faut voir quelles sont les sources de ces dépendances. Pour résumer, elles se situent au niveau des unités de calcul et du banc de registre.
===Les dépendances structurelles liées aux unités de calcul===
En théorie, une fois qu'une instruction est décodée, elle est envoyée dans la voie adaptée. Du moins, si cette voie est libre. En effet, l'usage d'instructions multicycles fait qu'une voie peut être occupée pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Et en général, des instructions consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs.
En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique.
La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle.
[[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]]
Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant.
===Les dépendances structurelles à l'écriture dans le banc de registre===
Les instructions multi-cycle lisent et écrivent dans le même banc de registres que les instructions 1-cycle. Le résultat est qu'elles peuvent écrire dans les registres en même temps. Par exemple, imaginons que l'on exécute une multiplication de 6 cycles, suivie au cycle suivant par une opération qui n'en prend que 5. Elles tenteront d'enregistrer leurs données en même temps 6 cycles après pour la première, 1 + 5 cycles pour l'autre. Elles voudront donc enregistre une donnée en même temps. Mais le banc de registre n'a qu'un seul port d'écriture...
La solution la plus simple est que la seconde instruction doit être retardée d'un cycle pour résoudre le problème. En soi, rien de problématique à gérer, l'unité de décodage a juste à insérer des bulles de pipeline. Une autre solution ajoute des ports d'écriture sur le banc de registres, afin que les deux instructions peuvent écrire en même temps dans le banc de registre. Il faut juste qu'elles écrivent dans des registres différents, mais il s'agit d'un cas de dépendance de données, pas une dépendance structurelle proprement dit.
La première solution a un cout en performance, mais est simple à implémenter. Son cout en transistor est limité, tout est concentré dans l'unité de décodage/émission. Pour la seconde solution, son cout en transistor est important, sans compter que le banc de registre devient alors plus gourmand en énergie. Le tout est à comparer à un gain en performance généralement limité. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul.
Les premiers processeurs MIPS avaient une technique très particulière pour éliminer les dépendances structurelles de ce genre, qui éliminait aussi partiellement les dépendances WAW. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications et éventuellement les divisions. Ils ajoutaient un aval séparé, un simple circuit multiplieur/diviseur séparé de l'ALU entière. Les instructions multi-cycles disposaient de leur propre set de registres rien que pour elles, ce qui résolvait pas mal de problèmes de conflits d'accès aux registres.
==L'inversion de l'ordre des écritures==
Un problème des pipelines dynamiques est qu'ils ne garantissent pas l'ordre des écritures. Si on émet des instructions dans l'ordre du programme, il se peut qu'elles ne terminent pas dans le même ordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé. Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre si elles n'ont pas de dépendances entre elles. On dit qu'il y a '''inversion de l'ordre des écritures'''.
[[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]]
L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle...
Au-delà des problèmes liés aux exceptions précises, les dépendances de données posent aussi problème. Si deux écritures se font dans des registres différents, peu importe l'ordre des écritures, tout se passera bien. Mais si les deux écritures se font dans le même registre, il y a un problème : le résultat est incorrect, un registre a été écrasé avec une valeur antérieure. Le problème en question correspond à l'apparition de '''dépendances de donnée de type WAW'''. Les dépendances WAW imposent un ordre concernant les écritures dans les registres et posent des problèmes sur les pipelines dynamiques, uniquement eux.
===Les registres d'échelonnage===
Pour conserver l'ordre des écritures, la solution la plus simple est de '''retarder les écritures''' jusqu'à ce qu'elles atteignent la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. L'implémentation est plus simple si on suppose que toutes les instructions font le même nombre de cycles. On parle alors d'un pipeline de longueur fixe. Le cout à payer est que les instructions doivent se caler sur l'instruction la plus lente. Si une addition met un cycle à s'exécuter et une multiplication 3, alors toutes les instructions font 3 cycles.
Si l'addition fournit son résultat en un cycle, il sera enregistré dans les registres avec un retard de deux cycles. Idem pour les lectures mémoire : si un accès mémoire fait 2 cycles, l'écriture de la donnée lue dans les registres sera retardée d'un cycle. Le retard dépend de l’opération à effectuer, en fonction de combien de cycles elle prend dans l'ALU. En clair, toutes les instructions ont le même nombre d'étages, mais certains étages sont inutiles pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire.
[[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multicycles]]
Reste à ajouter des cycles de retard, qui servent juste à retarder l'enregistrement dans les registres. Pour cela, on insère des registres entre la sortie de l'ALu et le banc de registre. Les registres en question sont appelés des ''staggering registers'', ou encore des '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe.
[[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]]
Un défaut de la méthode précédente est que les données sont copiées d'un registre à l'autre à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé.
[[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]]
Il faut rappeler que pour les écritures en RAM, le processeur utilise une file d'écriture pour garantir que les écritures se font dans l'ordre. Nous en avions parlé dans le chapitre sur le pipeline, quand nous avions parlé des exceptions précises. On peut la voir comme une sorte d'équivalent des registres d'échelonnage, mais pour les écritures en mémoire. La file d'écriture est séparée des registres d'échelonnage, pour une raison simple : elle ne communique pas avec les registres, mais avec le cache. Sa sortie étant différente, elle est donc séparée.
===Le tampon de réordonnancement===
Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit.
La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe.
[[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]]
Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats.
Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc.
[[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]]
Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée.
Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises.
{|class="wikitable"
|+ Une entrée du ROB
|-
| ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception
|}
Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ
Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans.
Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement.
===Le tampon d’historique===
Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB).
Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception.
[[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]]
===Le banc de registres futurs===
Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc.
Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit.
Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct.
Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.*
[[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]]
Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres.
===Les point de contrôle de registre===
La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements.
Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur.
Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde.
[[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]]
Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc.
La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF.
===La complétion dans le désordre===
Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''.
En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté.
Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il s’applique en absence de branchements et peut être vu comme une amélioration des lectures non-bloquantes. Le mécanisme s'active quand une lecture est bloquée par un défaut de cache ou toute autre situation. Le processeur implémente l'exécution dans le désordre, ce qui fait qu'il exécute les instructions qui suivent une lecture bloquée, à condition qu'elles ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en attente. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon.
Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU.
==La fréquence des avals des pipelines multicycle==
L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations.
===Le ''clock gating'' des avals inutilisés===
Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''.
Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache.
Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge.
La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie.
===Les unités de calcul à double fréquence du Pentium 4===
Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe.
L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel.
Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles.
La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les optimisations du chargement des instructions
| prevText=Les optimisations du chargement des instructions
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
q9guio6tl5bfdmdnh6crqo5yp6t81ru
774581
774580
2026-10-10T11:45:38Z
Mewtow
31375
/* Les registres d'échelonnage */
774581
wikitext
text/x-wiki
La quasi-totalité des processeurs, modernes comme anciens, supportent des '''instructions multi-cycle''', à savoir des instructions qui mettent plusieurs cycles à s'exécuter. Un exemple typique est celui des opérations flottantes ou des multiplications. Et gérer de telles instructions avec un pipeline pose de nombreux problèmes d'implémentation. Les instructions multi-cycles ne semblent pas vraiment coller avec un pipeline, qui a une longueur fixe, pour ce qu'on en sait pour le moment.
Pour le moment, nous avons vu des pipelines assez simples, comme le pipeline Fetch-Decode-Exec ou le pipeline RISC classique à 5 étages. De tels pipelines ont un nombre d'étages fixe, qui est le même pour toutes les instructions. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Un pipeline fixe peut supporter les instructions multicycles, mais cela demande d'utiliser des blocages de pipeline. Une instruction multicycle occupe l'étage d’exécution pendant plusieurs cycles, les étages précédents sont gelés pendant ce temps.
Dans ce chapitre, nous allons voir comment modifier un processeur à pipeline, pour qu'il gère des instructions multi-cycles sans utiliser de blocages. La modification donne un '''pipeline dynamique'''. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. L'implémentation d'un pipeline dynamique est assez simple : il y a juste besoin d'utiliser des unités de calcul dédiées pour les instructions lentes, avec par exemple un circuit multiplieur séparé d'une ALU entière. Le temps de calcul dépend de l'ALU, et donc le nombre de cycles de l'instruction associée.
[[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]]
==Les pipelines multi-cycles séparent ''front-end'' et ''back-end''==
Avec un pipeline dynamique, le processeur est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat.
[[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec un aval et un amont (back-end et front-end).]]
===La spécialisation des avals===
La quasi-totalité des processeurs modernes dispose d'une voie séparée pour les accès mémoire, une '''unité mémoire''' dédiée. L'unité mémoire est séparée pour une bonne raison : les accès mémoire se marient assez mal avec un pipeline. Les accès mémoire ont une latence élevée, variable. Ils sont difficiles à pipeliner de manière générale. La voie pour les accès mémoire sert donc d'exception, c'est une unité qui n'est pas tout à fait pipelinée, alors que le reste du chemin de données l'est. Sa connexion avec le pipeline varie grandement suivant le processeur.
[[File:Pipeline à deux aval de type load-store.png|centre|vignette|upright=1.5|Pipeline à deux aval de type load-store.]]
Si on omet les voies liées aux accès mémoire, les autres voies correspondent peu ou prou à une unité de calcul. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée.
La destination de chaque voie peut varier. Les instructions de calcul ou les lectures enregistrent un résultat dans les registres, et se terminent donc sur le port d'écriture du banc de registres. Par contre, les voies pour les branchements ou les écritures n'enregistrent rien dans les registres. La voie pour les branchements se termine dans le séquenceur et/ou dans le registre d'état, la voie pour les écritures ne finit nulle part si ce n'est dans l'unité mémoire.
[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|centre|vignette|upright=1.5|Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements]]
Plus tard dans la suite du cours, nous verrons que les processeurs modernes regroupent plusieurs ALUs par voie, pour des raisons assez compliquées à expliquer ici. Retenez juste que le chemin de données est composé de plusieurs voies relativement indépendantes, chacune spécialisée dans un type de micro-opération spécialisé. Les voies commencent toutes avec la lecture des opérandes, elles effectuent un calcul, puis enregistrent leurs résultats dans les registres si besoin.
===Les processeurs à émission simple et multiple===
Toujours est-il que le décodeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie.
Sur les processeurs dits à '''simple émission''', une instruction est envoyée dans une voie à chaque cycle, soit dans une ALU, soit dans l'unité mémoire, soit dans les voies pour les branchements, etc. Ils peuvent émettre au maximum une instruction apr cycle d'horloge, ils exécutent en moyenne une instruction par cycle. Diverses optimisations comme le renommage de registres et l'exécution dans le désordre permettent de se rapprocher de ce rythme de croisière d'une instruction lancée par cycle.
Mais il existe des processeurs à '''émission multiple''' qui sont capables d'émettre plusieurs instructions à la fois, chacune dans des voies séparées. Les processeurs dits superscalaires sont dans ce cas, mais ils ne sont pas les seuls. Ils permettent de gagner en performance en utilisant au maximum les unités de calcul/voies. Si plusieurs unités de calcul sont inoccupées, un processeur à émission multiple pourra lancer une instruction dans chacune d'entre elles en même temps, ce qui permet de les occuper le plus vite possible. De moins, c'est le cas si les conditions adéquates sont réunies, mais laissons cela pour plus tard. Un chapitre entier sera dédié aux processeurs à émission multiple.
==Les dépendances structurelles : l'occupation de chaque voie==
Les pipelines multi-cycles doivent gérer des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Les dépendances structurelles sont généralement assez rares, car les pipelines prennent bien garde à bien séparer les étages du pipeline, en dupliquant des circuits s'il le faut. Par exemple, séparer le cache en un cache d'instruction et un cache de donnée élimine une dépendance structurelle. Mais sur les pipelines dynamiques, c'est une autre histoire. Mais pour comprendre pourquoi, il faut voir quelles sont les sources de ces dépendances. Pour résumer, elles se situent au niveau des unités de calcul et du banc de registre.
===Les dépendances structurelles liées aux unités de calcul===
En théorie, une fois qu'une instruction est décodée, elle est envoyée dans la voie adaptée. Du moins, si cette voie est libre. En effet, l'usage d'instructions multicycles fait qu'une voie peut être occupée pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Et en général, des instructions consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs.
En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique.
La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle.
[[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]]
Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant.
===Les dépendances structurelles à l'écriture dans le banc de registre===
Les instructions multi-cycle lisent et écrivent dans le même banc de registres que les instructions 1-cycle. Le résultat est qu'elles peuvent écrire dans les registres en même temps. Par exemple, imaginons que l'on exécute une multiplication de 6 cycles, suivie au cycle suivant par une opération qui n'en prend que 5. Elles tenteront d'enregistrer leurs données en même temps 6 cycles après pour la première, 1 + 5 cycles pour l'autre. Elles voudront donc enregistre une donnée en même temps. Mais le banc de registre n'a qu'un seul port d'écriture...
La solution la plus simple est que la seconde instruction doit être retardée d'un cycle pour résoudre le problème. En soi, rien de problématique à gérer, l'unité de décodage a juste à insérer des bulles de pipeline. Une autre solution ajoute des ports d'écriture sur le banc de registres, afin que les deux instructions peuvent écrire en même temps dans le banc de registre. Il faut juste qu'elles écrivent dans des registres différents, mais il s'agit d'un cas de dépendance de données, pas une dépendance structurelle proprement dit.
La première solution a un cout en performance, mais est simple à implémenter. Son cout en transistor est limité, tout est concentré dans l'unité de décodage/émission. Pour la seconde solution, son cout en transistor est important, sans compter que le banc de registre devient alors plus gourmand en énergie. Le tout est à comparer à un gain en performance généralement limité. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul.
Les premiers processeurs MIPS avaient une technique très particulière pour éliminer les dépendances structurelles de ce genre, qui éliminait aussi partiellement les dépendances WAW. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications et éventuellement les divisions. Ils ajoutaient un aval séparé, un simple circuit multiplieur/diviseur séparé de l'ALU entière. Les instructions multi-cycles disposaient de leur propre set de registres rien que pour elles, ce qui résolvait pas mal de problèmes de conflits d'accès aux registres.
==L'inversion de l'ordre des écritures==
Un problème des pipelines dynamiques est qu'ils ne garantissent pas l'ordre des écritures. Si on émet des instructions dans l'ordre du programme, il se peut qu'elles ne terminent pas dans le même ordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé. Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre si elles n'ont pas de dépendances entre elles. On dit qu'il y a '''inversion de l'ordre des écritures'''.
[[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]]
L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle...
Au-delà des problèmes liés aux exceptions précises, les dépendances de données posent aussi problème. Si deux écritures se font dans des registres différents, peu importe l'ordre des écritures, tout se passera bien. Mais si les deux écritures se font dans le même registre, il y a un problème : le résultat est incorrect, un registre a été écrasé avec une valeur antérieure. Le problème en question correspond à l'apparition de '''dépendances de donnée de type WAW'''. Les dépendances WAW imposent un ordre concernant les écritures dans les registres et posent des problèmes sur les pipelines dynamiques, uniquement eux.
===Les registres d'échelonnage===
Pour conserver l'ordre des écritures, la solution la plus simple est de '''retarder les écritures dans les registres''', jusqu'à ce qu'elles atteignent la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. L'implémentation est plus simple si on suppose que toutes les instructions font le même nombre de cycles. On parle alors d'un pipeline de longueur fixe. Le cout à payer est que les instructions doivent se caler sur l'instruction la plus lente. Si une addition met un cycle à s'exécuter et une multiplication 3, alors toutes les instructions font 3 cycles.
Si l'addition fournit son résultat en un cycle, il sera enregistré dans les registres avec un retard de deux cycles. Idem pour les lectures mémoire : si un accès mémoire fait 2 cycles, l'écriture de la donnée lue dans les registres sera retardée d'un cycle. Le retard dépend de l’opération à effectuer, en fonction de combien de cycles elle prend dans l'ALU. En clair, toutes les instructions ont le même nombre d'étages, mais certains étages sont inutiles pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire.
[[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multicycles]]
Reste à ajouter des cycles de retard, qui servent juste à retarder l'enregistrement dans les registres. Pour cela, on insère des registres entre la sortie de l'ALu et le banc de registre. Les registres en question sont appelés des ''staggering registers'', ou encore des '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe.
[[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]]
Un défaut de la méthode précédente est que les données sont copiées d'un registre à l'autre à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé.
[[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]]
Il faut rappeler que pour les écritures en RAM, le processeur utilise une file d'écriture pour garantir que les écritures se font dans l'ordre. Nous en avions parlé dans le chapitre sur le pipeline, quand nous avions parlé des exceptions précises. On peut la voir comme une sorte d'équivalent des registres d'échelonnage, mais pour les écritures en mémoire. La file d'écriture est séparée des registres d'échelonnage, pour une raison simple : elle ne communique pas avec les registres, mais avec le cache. Sa sortie étant différente, elle est donc séparée.
===Le tampon de réordonnancement===
Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit.
La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe.
[[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]]
Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats.
Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc.
[[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]]
Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée.
Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises.
{|class="wikitable"
|+ Une entrée du ROB
|-
| ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception
|}
Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ
Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans.
Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement.
===Le tampon d’historique===
Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB).
Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception.
[[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]]
===Le banc de registres futurs===
Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc.
Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit.
Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct.
Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.*
[[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]]
Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres.
===Les point de contrôle de registre===
La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements.
Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur.
Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde.
[[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]]
Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc.
La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF.
===La complétion dans le désordre===
Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''.
En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté.
Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il s’applique en absence de branchements et peut être vu comme une amélioration des lectures non-bloquantes. Le mécanisme s'active quand une lecture est bloquée par un défaut de cache ou toute autre situation. Le processeur implémente l'exécution dans le désordre, ce qui fait qu'il exécute les instructions qui suivent une lecture bloquée, à condition qu'elles ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en attente. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon.
Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU.
==La fréquence des avals des pipelines multicycle==
L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations.
===Le ''clock gating'' des avals inutilisés===
Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''.
Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache.
Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge.
La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie.
===Les unités de calcul à double fréquence du Pentium 4===
Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe.
L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel.
Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles.
La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les optimisations du chargement des instructions
| prevText=Les optimisations du chargement des instructions
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
4ewz9c0s4o28jecxkgdahhhpxgiulhy
774582
774581
2026-10-10T11:45:54Z
Mewtow
31375
/* L'inversion de l'ordre des écritures */
774582
wikitext
text/x-wiki
La quasi-totalité des processeurs, modernes comme anciens, supportent des '''instructions multi-cycle''', à savoir des instructions qui mettent plusieurs cycles à s'exécuter. Un exemple typique est celui des opérations flottantes ou des multiplications. Et gérer de telles instructions avec un pipeline pose de nombreux problèmes d'implémentation. Les instructions multi-cycles ne semblent pas vraiment coller avec un pipeline, qui a une longueur fixe, pour ce qu'on en sait pour le moment.
Pour le moment, nous avons vu des pipelines assez simples, comme le pipeline Fetch-Decode-Exec ou le pipeline RISC classique à 5 étages. De tels pipelines ont un nombre d'étages fixe, qui est le même pour toutes les instructions. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Un pipeline fixe peut supporter les instructions multicycles, mais cela demande d'utiliser des blocages de pipeline. Une instruction multicycle occupe l'étage d’exécution pendant plusieurs cycles, les étages précédents sont gelés pendant ce temps.
Dans ce chapitre, nous allons voir comment modifier un processeur à pipeline, pour qu'il gère des instructions multi-cycles sans utiliser de blocages. La modification donne un '''pipeline dynamique'''. Avec un pipeline dynamique, certaines instructions peuvent prendre 1 cycle, d'autres 7 cycles, d'autres 9, d'autres 25, etc. Les instructions rapides prennent moins de cycles que les autres. L'implémentation d'un pipeline dynamique est assez simple : il y a juste besoin d'utiliser des unités de calcul dédiées pour les instructions lentes, avec par exemple un circuit multiplieur séparé d'une ALU entière. Le temps de calcul dépend de l'ALU, et donc le nombre de cycles de l'instruction associée.
[[File:Pipeline avec un nomrbe variable d'étages par instructions.png|centre|vignette|upright=3|Pipeline avec un nombre variable d'étages par instructions.]]
==Les pipelines multi-cycles séparent ''front-end'' et ''back-end''==
Avec un pipeline dynamique, le processeur est décomposé en deux parties. La première partie, l’'''amont''' (''front end''), prend en charge les étages communs à toutes les instructions. Il correspond au séquenceur et à l'unité de chargement et regroupe la mise à jour du ''program counter'', le chargement, l'étage de décodage, etc. L'amont est suivi par le chemin de données, qui est organisée en plusieurs voies, chacune formant ce qu'on appelle un '''aval''' (''back end''), ou encore une '''voie'''. Un aval correspond soit à une unité de calcul, soit à l'unité mémoire, soit à une unité pour les branchements. Toutes les voies partagent le banc de registre, que ce soit pour lire les opérandes ou enregistrer un résultat.
[[File:Pipeline avec un aval et un amont (back-end et front-end).png|centre|vignette|upright=1.5|Pipeline avec un aval et un amont (back-end et front-end).]]
===La spécialisation des avals===
La quasi-totalité des processeurs modernes dispose d'une voie séparée pour les accès mémoire, une '''unité mémoire''' dédiée. L'unité mémoire est séparée pour une bonne raison : les accès mémoire se marient assez mal avec un pipeline. Les accès mémoire ont une latence élevée, variable. Ils sont difficiles à pipeliner de manière générale. La voie pour les accès mémoire sert donc d'exception, c'est une unité qui n'est pas tout à fait pipelinée, alors que le reste du chemin de données l'est. Sa connexion avec le pipeline varie grandement suivant le processeur.
[[File:Pipeline à deux aval de type load-store.png|centre|vignette|upright=1.5|Pipeline à deux aval de type load-store.]]
Si on omet les voies liées aux accès mémoire, les autres voies correspondent peu ou prou à une unité de calcul. Par exemple, il est fréquent d'avoir des voies séparés pour l'addition, la multiplication et les décalages (''barrel shifter''), vu que ces opérations sont dans des ALU entières séparées. De même, on a une voie séparée pour les instructions flottantes, lié au fait que la FPU est une unité de calcul séparée.
La destination de chaque voie peut varier. Les instructions de calcul ou les lectures enregistrent un résultat dans les registres, et se terminent donc sur le port d'écriture du banc de registres. Par contre, les voies pour les branchements ou les écritures n'enregistrent rien dans les registres. La voie pour les branchements se termine dans le séquenceur et/ou dans le registre d'état, la voie pour les écritures ne finit nulle part si ce n'est dans l'unité mémoire.
[[File:Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements.png|centre|vignette|upright=1.5|Pipeline avec un aval pour les instructions arithmétiques, un aval pour les accès mémoire et un aval pour les branchements]]
Plus tard dans la suite du cours, nous verrons que les processeurs modernes regroupent plusieurs ALUs par voie, pour des raisons assez compliquées à expliquer ici. Retenez juste que le chemin de données est composé de plusieurs voies relativement indépendantes, chacune spécialisée dans un type de micro-opération spécialisé. Les voies commencent toutes avec la lecture des opérandes, elles effectuent un calcul, puis enregistrent leurs résultats dans les registres si besoin.
===Les processeurs à émission simple et multiple===
Toujours est-il que le décodeur envoie des instructions décodées dans la voie appropriée. Par exemple, une instruction d'accès mémoire va dans l'unité mémoire, dans la voie spécialisée dans les accès mémoire. Une instruction de calcul va dans la voie spécialisée dans les calculs entiers simples, à savoir l'ALU entière. Et ainsi de suite. L'envoi d'une instruction/micro-opération dans une voie s'appelle l''''émission'''. On dit qu'une instruction est émise quand elle est envoyée dans une voie.
Sur les processeurs dits à '''simple émission''', une instruction est envoyée dans une voie à chaque cycle, soit dans une ALU, soit dans l'unité mémoire, soit dans les voies pour les branchements, etc. Ils peuvent émettre au maximum une instruction apr cycle d'horloge, ils exécutent en moyenne une instruction par cycle. Diverses optimisations comme le renommage de registres et l'exécution dans le désordre permettent de se rapprocher de ce rythme de croisière d'une instruction lancée par cycle.
Mais il existe des processeurs à '''émission multiple''' qui sont capables d'émettre plusieurs instructions à la fois, chacune dans des voies séparées. Les processeurs dits superscalaires sont dans ce cas, mais ils ne sont pas les seuls. Ils permettent de gagner en performance en utilisant au maximum les unités de calcul/voies. Si plusieurs unités de calcul sont inoccupées, un processeur à émission multiple pourra lancer une instruction dans chacune d'entre elles en même temps, ce qui permet de les occuper le plus vite possible. De moins, c'est le cas si les conditions adéquates sont réunies, mais laissons cela pour plus tard. Un chapitre entier sera dédié aux processeurs à émission multiple.
==Les dépendances structurelles : l'occupation de chaque voie==
Les pipelines multi-cycles doivent gérer des '''dépendances structurelles''', à savoir le cas où deux instructions veulent utiliser le même circuit en même temps. Les dépendances structurelles sont généralement assez rares, car les pipelines prennent bien garde à bien séparer les étages du pipeline, en dupliquant des circuits s'il le faut. Par exemple, séparer le cache en un cache d'instruction et un cache de donnée élimine une dépendance structurelle. Mais sur les pipelines dynamiques, c'est une autre histoire. Mais pour comprendre pourquoi, il faut voir quelles sont les sources de ces dépendances. Pour résumer, elles se situent au niveau des unités de calcul et du banc de registre.
===Les dépendances structurelles liées aux unités de calcul===
En théorie, une fois qu'une instruction est décodée, elle est envoyée dans la voie adaptée. Du moins, si cette voie est libre. En effet, l'usage d'instructions multicycles fait qu'une voie peut être occupée pendant plusieurs cycles. Un exemple est celui où on veut faire deux multiplications à la suite, en supposant qu'une multiplication fasse 5 cycles : la seconde doit attendre que la première se termine car le multiplieur est occupé pendant ce temps. Et en général, des instructions consécutives ne peuvent pas utiliser la même voie, la même ALU. La voie sera occupée par la première instruction, les autres devront attendre que la première ait terminé ses calculs.
En théorie, il est possible d'éliminer totalement ces dépendances structurelles. Une solution simple duplique l'unité de calcul fautive. Prenons un exemple où c'est le circuit multiplieur qui est fautif, où la multiplication prend cinq cycles sur le processeur. Avec une seule ALU, on doit attendre qu'une multiplication en cours soit terminée avant de lancer la suivante. Mais il est possible de lancer une nouvelle multiplication à chaque cycle si on dispose de 5 circuits multiplieurs : on lance une nouvelle multiplication à chaque cycle dans un multiplier différent. Mais le cout en transistors est prohibitif, surtout que le gain en performance est généralement faible. Il est en effet rare que 5 multiplications soient lancées à la suite dans le pipeline, ce qui fait que si on utilisait 5 multiplieurs, ils seraient sous-utilisés en pratique.
La seconde solution pipeline, échelonner les unités de calcul. Si jamais vous avez une opération qui prend cinq cycles, pourquoi ne pas lui fournir un seul circuit échelonné en cinq étages ? Pour certains circuits, c'est possible : on peut totalement échelonner une unité de multiplication, par exemple. En faisant ainsi, il est possible de démarrer une nouvelle multiplication à chaque cycle d'horloge dans la même ALU, éliminant ainsi toute dépendance structurelle.
[[File:Exemple avec une unité de multiplication séparée, totalement échelonnée.png|centre|vignette|upright=2|Exemple avec une unité de multiplication séparée, totalement pipelinée.]]
Dans la réalité, les statistiques montrent qu'il est rare que deux instructions multicycles se suivent dans un programme. La seule exception étant les instructions mémoire, mais nous les mettons à part pour le moment. En conséquence, les dépendances structurelles ont un cout en performance assez mineur. Échelonner un circuit permet un gain de performance pour un cout en circuit modéré : il suffit d'ajouter quelques registres dans le circuit. Le problème est que certaines instructions s’échelonnent mal, notamment les divisions. Aussi, de nombreux concepteurs de processeurs laissent quelques dépendances structurelles. Nous verrons comment elles sont gérées au chapitre suivant.
===Les dépendances structurelles à l'écriture dans le banc de registre===
Les instructions multi-cycle lisent et écrivent dans le même banc de registres que les instructions 1-cycle. Le résultat est qu'elles peuvent écrire dans les registres en même temps. Par exemple, imaginons que l'on exécute une multiplication de 6 cycles, suivie au cycle suivant par une opération qui n'en prend que 5. Elles tenteront d'enregistrer leurs données en même temps 6 cycles après pour la première, 1 + 5 cycles pour l'autre. Elles voudront donc enregistre une donnée en même temps. Mais le banc de registre n'a qu'un seul port d'écriture...
La solution la plus simple est que la seconde instruction doit être retardée d'un cycle pour résoudre le problème. En soi, rien de problématique à gérer, l'unité de décodage a juste à insérer des bulles de pipeline. Une autre solution ajoute des ports d'écriture sur le banc de registres, afin que les deux instructions peuvent écrire en même temps dans le banc de registre. Il faut juste qu'elles écrivent dans des registres différents, mais il s'agit d'un cas de dépendance de données, pas une dépendance structurelle proprement dit.
La première solution a un cout en performance, mais est simple à implémenter. Son cout en transistor est limité, tout est concentré dans l'unité de décodage/émission. Pour la seconde solution, son cout en transistor est important, sans compter que le banc de registre devient alors plus gourmand en énergie. Le tout est à comparer à un gain en performance généralement limité. La solution est surtout utilisée sur les processeurs avec beaucoup d'unités de calcul.
Les premiers processeurs MIPS avaient une technique très particulière pour éliminer les dépendances structurelles de ce genre, qui éliminait aussi partiellement les dépendances WAW. Ils utilisaient un pipeline RISC classique modifié pour supporter des instructions multi-cycles, à savoir des multiplications et éventuellement les divisions. Ils ajoutaient un aval séparé, un simple circuit multiplieur/diviseur séparé de l'ALU entière. Les instructions multi-cycles disposaient de leur propre set de registres rien que pour elles, ce qui résolvait pas mal de problèmes de conflits d'accès aux registres.
==L'inversion de l'ordre des écritures==
Un problème des pipelines dynamiques est qu'ils ne garantissent pas l'ordre des écritures, dans les registres. Si on émet des instructions dans l'ordre du programme, il se peut qu'elles ne terminent pas dans le même ordre. Par exemple, prenons l'exemple d'une multiplication prenant 6 cycles, suivie par une addition s’exécutant en un cycle. Le résultat est illustré ci-dessous : l'ordre d'enregistrement des résultats des instructions s'est inversé. Les instructions sont émises une par une, dans l'ordre, mais l'enregistrement des résultats dans les registres se font dans le désordre si elles n'ont pas de dépendances entre elles. On dit qu'il y a '''inversion de l'ordre des écritures'''.
[[File:Exemple de problème survenant avec des pipeline de longueur variable.png|centre|vignette|upright=2|Exemple de problème survenant avec des pipeline de longueur variable]]
L'inversion de l'ordre des écritures pose problème pour supporter les exceptions précises. Il est possible qu'une instruction lève une exception alors qu'une instruction suivante a déjà enregistré son résultat. Imaginez par exemple qu'une multiplication soit suivie par une addition. Imaginez que la multiplication lève une exception matérielle du type "débordement d'entier", 3 cycles après avoir démarré. L'addition aura déjà enregistré son résultat, vu qu'elle se fait en un seul cycle...
Au-delà des problèmes liés aux exceptions précises, les dépendances de données posent aussi problème. Si deux écritures se font dans des registres différents, peu importe l'ordre des écritures, tout se passera bien. Mais si les deux écritures se font dans le même registre, il y a un problème : le résultat est incorrect, un registre a été écrasé avec une valeur antérieure. Le problème en question correspond à l'apparition de '''dépendances de donnée de type WAW'''. Les dépendances WAW imposent un ordre concernant les écritures dans les registres et posent des problèmes sur les pipelines dynamiques, uniquement eux.
===Les registres d'échelonnage===
Pour conserver l'ordre des écritures, la solution la plus simple est de '''retarder les écritures dans les registres''', jusqu'à ce qu'elles atteignent la toute fin du pipeline. Ainsi, il n'y a pas d'inversion d'ordre des écritures dans les registres, ni d'écritures simultanées. L'implémentation est plus simple si on suppose que toutes les instructions font le même nombre de cycles. On parle alors d'un pipeline de longueur fixe. Le cout à payer est que les instructions doivent se caler sur l'instruction la plus lente. Si une addition met un cycle à s'exécuter et une multiplication 3, alors toutes les instructions font 3 cycles.
Si l'addition fournit son résultat en un cycle, il sera enregistré dans les registres avec un retard de deux cycles. Idem pour les lectures mémoire : si un accès mémoire fait 2 cycles, l'écriture de la donnée lue dans les registres sera retardée d'un cycle. Le retard dépend de l’opération à effectuer, en fonction de combien de cycles elle prend dans l'ALU. En clair, toutes les instructions ont le même nombre d'étages, mais certains étages sont inutiles pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire.
[[File:Processeur à pipeline fixe qui gére des instructions multicycles.png|centre|vignette|upright=3|Processeur à pipeline fixe qui gére des instructions multicycles]]
Reste à ajouter des cycles de retard, qui servent juste à retarder l'enregistrement dans les registres. Pour cela, on insère des registres entre la sortie de l'ALu et le banc de registre. Les registres en question sont appelés des ''staggering registers'', ou encore des '''registres d'échelonnage'''. Il y a autant de registres d'échelonnage que cycles de retard à ajouter, car chacun retarde le résultat d'un cycle d’horloge. Pendant les cycles de retard, le résultat sera passé d'un registre d'échelonnage à l'autre, sans que rien ne se passe.
[[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Registres d'échelonnage]]
Un défaut de la méthode précédente est que les données sont copiées d'un registre à l'autre à chaque cycle d'horloge, ce qui consomme de l'énergie pour rien. Pour éviter cela, les processeurs modernes regroupent les registres d'échelonnage dans un banc de registre séparé, appelé le '''banc de registres d'échelonnage'''. Lors du décodage de l'instruction, un registre est attribué à l'instruction pour mémoriser son résultat, si besoin. Le résultat est enregistré dans le registre alloué en sortant de l'unité de calcul. Puis, il est copié dans le banc de registre architectural dès que le délai nécessaire est écoulé.
[[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]]
Il faut rappeler que pour les écritures en RAM, le processeur utilise une file d'écriture pour garantir que les écritures se font dans l'ordre. Nous en avions parlé dans le chapitre sur le pipeline, quand nous avions parlé des exceptions précises. On peut la voir comme une sorte d'équivalent des registres d'échelonnage, mais pour les écritures en mémoire. La file d'écriture est séparée des registres d'échelonnage, pour une raison simple : elle ne communique pas avec les registres, mais avec le cache. Sa sortie étant différente, elle est donc séparée.
===Le tampon de réordonnancement===
Le '''tampon de réordonnancement''' est la technique phare utilisée pour remettre les écritures dans l'ordre. L'idée est là encore de remettre les écritures dans l'ordre. Les micro-opérations fournissent leurs résultats dans le désordre, mais les résultats sont mis en attente, afin que les écritures se fassent dans l'ordre. La mise en attente se fait dans une mémoire appelée le tampon de ré-ordonnancement, ''Re-order buffer'' en anglais, ce qui fait que j'utiliserais parfois l'abréviation ROB dans ce qui suit.
La différence avec les registres d’échelonnement est que la mise en attente est plus flexible, elle ne demande pas que toutes les instructions prennent le même nombre de cycles pour s'exécuter. Une instruction peut enregistrer son résultat dès que les précédentes sont terminées, au cycle précis où elles ont toutes enregistrées leurs résultats. Dit autrement, seule l'instruction la plus ancienne peut quitter le ROB et enregistrer son résultat, les autres instructions doivent attendre. De ce point de vue, le tampon de réordonnancent est donc une mémoire FIFO. On peut la voir comme une amélioration de la technique précédente, où les registres d’échelonnement sont remplacés par une structure matérielle plus complexe.
[[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]]
Les résultats sont enregistrés dans le désordre dans le ROB, mais finissent triés par l'ordre du programme. L'ordre d'écriture demande de mémoriser l'ordre d'exécution des instructions. Pour cela, on profite du fait que les instructions sont décodées dans l'ordre. Après les décodeurs se trouve un circuit appelé l'unité d'émission, qu'on détaillera dans le chapitre suivant. Pour simplifier, elle vérifie que l'instruction qu'elle reçoit peut s'exécuter, et l'envoie aux unités de calcul si c'est le cas. L'unité d'émission ajoute l'instruction au ROB quand elle l'envoie au chemin de données. Si le ROB est plein, on ne peut pas y ajouter de nouvelle instruction. En conséquence, le processeur bloque les étages de chargement, décodage, etc. Le processeur est débloqué quand une instruction enregistre ses résultats.
Un processeur avec un tampon de ré-ordonnancement est donc composé de trois sections : un ''front-end'' qui regroupe chargement et décodage, un chemin de données (ALU et registres), et un système complémentaire de ré-ordonnancement. Ce dernier est composé du tampon de ré-ordonnancement, de l'unité d'émission et de l'étage de ''writeback'' pour l'enregistrement dans les registres. Le ROB envoie toutes les informations nécessaires à l'étage de ''Writeback'', à savoir le registre de destination, l'indicateur d'exception, le résultat à enregistrer, etc.
[[File:Processeur avec un tampon de ré-ordonnancement.png|centre|vignette|upright=2|Processeur avec un tampon de ré-ordonnancement]]
Les résultats sont enregistrés dans le ROB dans le désordre. Cependant, le ROB fait le lien entre un résultat et l'instruction qui l'a produite, il associe une instruction avec son résultat. Expliquer comment est fait ce lien demande d'expliquer comment est implémenté le ROB. Pour simplifier, c'est un subtil mélange entre une mémoire cache et une mémoire FIFO. Le ROB est composé d''''entrées''', chacune contient de quoi faire le lien entre un résultat et l'instruction associée.
Une entrée contient le ''program counter'' associé à l'instruction, le registre de destination du résultat, un champ pour mémoriser le résultat, et un ''bit de présence''. Ce dernier indique si le résultat a bien été calculé. Le champ résultat est initialement laissé vide, mais le résultat de l'instruction est copié dedans une fois qu'il est disponible. Notons que certaines instructions ne renvoient pas de résultat, comme les branchements ou les écritures en mémoire, mais on leur alloue quand même une entrée dans le ROB. Pour cela, le bit de présence est couplé à un bit qui indique si l'instruction doit fournir un résultat ou non. Les deux sont regroupés sous le noms de bits de présence. Si le processeur gère des exceptions précises, il faut ajouter un champ pour l'indicateur d'exception, qui est utilisé pour gérer des exceptions précises.
{|class="wikitable"
|+ Une entrée du ROB
|-
| ''Program counter'' || Registre de destination || Champ résultat || Bits de présence || Champ exception
|}
Lorsqu'une instruction est émise, elle réserve une entrée du ROB, pour que son résultat soit ajouté dedans ultérieurement. L'émission se faisant dans l'ordre, les entrées du ROB sont allouées dans l'ordre. Le ROB est donc une sorte de mémoire FIFO du point de vue des instructions. Une instruction réserve une entrée du ROB en écrivant son ''Program counter'' dedans, et en remplissant le champ pour le registre de destination. Le champ
Par la suite, les différents résultats vont être enregistrés dans les entrées adéquates, dans l'entrée réservée par l'instruction. Pour faire le lien entre un résultat et une instruction, le ROB fonctionne comme un cache d'instruction dont le ''tag'' serait le ''program counter''. Le ''program counter'' est propagé dans le pipeline, d'étage en étage, ce qui fait que le résultat sortira de l'unité de calcul en étant associé à ce ''program counter''. Le ''program counter'' est alors utilisé pour accéder au ROB comme on le ferait avec un cache d'instruction. L'entrée qui déclenche un succès de cache est alors sélectionnée : le résultat est copié dedans.
Une fois que toutes les instructions précédentes sont terminées, l'instruction peut enregistrer son résultat dans les registres. instruction quitte alors le ROB, dans le sens où l'entrée est vidée. Le résultat à enregistrer est lu depuis le champ résultat, le registre de destination est lu depuis le champ du même nom. L'étage de ''Writeback'' vérifie que le résultat est disponible en vérifiant les bits de présence. L'étage de ''writeback'' peut aussi vérifier si l'instruction a levé une exception matérielle, en regardant le champ Exception. Si une exception a lieu, cela signifie que les instructions suivantes sont invalides. Le ROB est alors vidé, ce qui le débarrasse des instruction invalides : leurs résultats ne seront pas enregistrés dans les registres architecturaux. Le même mécanisme est utilisé pour gérer les mauvaises prédictions de branchement.
===Le tampon d’historique===
Une autre solution laisse les instructions écrire dans les registres dans l'ordre qu'elles veulent, mais conserve des informations pour remettre les écritures dans l'ordre, pour retrouver les valeurs antérieures. Ces informations sont stockées dans ce qu'on appelle le '''tampon d’historique''' (''history buffer'' ou HB).
Le tampon d'historique est une mémoire LIFO dont chaque mot mémoire est une entrée qui mémorise les informations dédiées à une instruction. Lorsqu'une instruction modifie un registre, le HB sauvegarde une copie de l'ancienne valeur, pour la restaurer en cas d'exception. Pour annuler les modifications faites par des instructions exécutées à tort, on utilise le contenu de l'HB pour remettre les registres à leur ancienne valeur. Plus précisément, on vide le HB dans l'ordre inverse d'ajout des instructions, en allant de la plus récente à la plus ancienne, jusqu'à vider totalement le HB. Une fois le tout terminé, on retrouve bien les registres tels qu'ils étaient avant l’exécution de l'exception.
[[File:Tampon d’historique.png|centre|vignette|upright=2|Tampon d’historique.]]
===Le banc de registres futurs===
Le ROB et le HB sont deux techniques opposées sur le principe. Le ROB part du principe assez pessimiste que le banc de registre doit conserver un état propre, capable de gérer des exceptions précises. L'état temporaire est alors stocké dans le ROB, afin de pouvoir être annulé en cas de souci. La récupération en cas d'exception/branchement est alors assez rapide, mais le cout d'implémentation est assez important. Le HB fait l'inverse, avec une technique optimiste. Le HB enregistre directement l'état temporaire dans le banc de registre, mais mémorise de quoi revenir en arrière. Avec un HB, remettre les registres à l'état normal prend du temps. Deux techniques assez opposées, donc.
Il existe une solution intermédiaire, qui consiste à utiliser à la fois un ROB et un HB. La technique utilise deux bancs de registres. Le premier est mis à jour comme si les exceptions n’existaient pas, et conserve un état spéculatif : c'est le '''banc de registres futurs''' (future file ou FF). Il fonctionne plus ou moins comme l'''History Buffer'' de la section précédente. L'autre stocke les données valides en cas d'exception : c'est le '''banc de registres de retrait''' (retirement register file ou RRF). Il fonctionne sur le même principe que le banc de registre avec un ROB. Il est d'ailleurs couplé à un ROB, histoire de conserver un état valide en cas d'exception. Mais ce ROB est simplifié, comme on va le voir dans ce qui suit.
Le FF est systématiquement utilisé pour l'exécution des instructions. Dès qu'on doit exécuter des instructions, c'est dans ce banc de registre que sont lues les opérandes. La raison est que ce banc de registre contient les dernières données calculées. Notons que dans la technique utilisant seulement un ROB, les opérandes auraient été lues depuis le ROB. Mais là, le ROB n'est plus connecté aux entrées de l'ALU, seul le FF l'est. Le câblage est donc plus simple et l'implémentation facilitée. Le RRF est quant à lui utilisé en cas d'exception ou de branchement, pour récupérer l'état correct.
Après une exception ou un branchement, deux méthodes sont possibles. Avec la première, les opérandes sont lues depuis ce banc de registre, jusqu'à ce que le FF soit de nouveau utilisable. Mais détecter quel banc de registre utiliser est assez compliqué. Elle n'est en pratique pas implémentée, car demandant trop de circuits. L'autre solution est de recopier le contenu du RRF dans le FF. Là encore, le temps de recopie est assez long, sauf si on utilise certaines optimisations des bancs de registre. Il existe en effet des méthodes pour copier un banc de registre entier dans un autre en à peine un ou deux cycles d'horloge. Elles sont assez compliquées et on ne peut pas les expliquer ici simplement.*
[[File:Banc de registres futurs.png|centre|vignette|upright=2|Banc de registres futurs.]]
Une variante de cette technique a été utilisée sur le processeur Pentium 4. La différence avec la technique présentée est l'usage du renommage de registre, qui permettait de se passer de deux bancs de registres proprement dit. Les deux bancs de registres étaient inclus dans un banc de registre beaucoup plus grand. Mais nous détaillerons cela dans quelques chapitres.
===Les point de contrôle de registre===
La technique du '''''register checkpointing''''' est une technique qui marche surtout pour les branchements, mais ne gére pas les exceptions précises, du moins pas dans sa version la plus simple. Elle peut être adaptée pour gérer des exceptions précise, mais nous allons simplement voir une version qui gère uniquement les branchements.
Elle consiste à sauvegarder les registres quand un branchement est décodé, pour ensuite restaurer les registres si besoin. Tous les registres architecturaux du processeur sont sauvegardés, et parfois quelques registres microarchitecturaux. En clair, une copie intégrale du banc de registre est réalisée, le registre d'état est lui aussi sauvegardé, etc. La sauvegarde des registres porte le nom de '''point de contrôle de registre''', nous dirons simplement "point de contrôle". Le point de contrôle est stocké dans un autre banc de registre, séparé du banc de registre principal, qui est complétement invisible pour le programmeur.
Un point de contrôle est pris au moment où un branchement est décodé. On ne sait pas si ce branchement sera pris ou non, ce qui fait les instructions qui vont suivre peuvent être correcte si le branchement n'est pas pris, ou incorrectes si le branchement est pris et saute ailleurs dans le programme. Le branchement s'exécute normalement, le banc de registre est modifié par les instructions qui le suivent, tout comme c'est le cas avec un HB. Vers la fin du pipeline, on regarde si le branchement est pris ou non. S'il n'est pas pris, il n'y a rien à faire : les registres contiennent des données valides. Mais si le branchement est pris, alors les registres contiennent des valeurs invalides. Le point de contrôle est restauré, à savoir que le banc de registre est restauré dans le même état qu'un moment du point de sauvegarde.
[[File:Register checkpointing.png|centre|vignette|upright=1.5|Register checkpointing]]
Il est intéressant de comparer cette technique à la technique du tampon d'historique. Le principe est le même : on laisse les instructions modifier les registres, mais on doit annuler ces modifications en revenant en arrière. Sauf que le tampon d'historique restaure les registres un par un. Alors qu'avec un point de contrôle, la restauration du banc de registre se fait en bloc : on restaure tous les registres d'un seul coup, en un seul cycle. Une autre différence est que le point de contrôle ne s’embarrasse pas à savoir quels registres ont été modifiés à tord ou non, tous les registres sont sauvegardés et restaurés en un ou deux cycles d'horloge. Bien sûr, cela demande des bancs de registre spécialement conçus pour. Tout se passe comme si le tampon d'historique étant remplacé par un second banc de registre, avec une procédure de restauration optimisée se faisant en bloc.
La technique peut aussi être comparée avec la technique du banc de registres futurs, elle-même très liée au tampon d'historique. L'idée est que le point de contrôle est le RRF (banc de registre de retirement), alors que le banc de registre normal est un FF (banc de registre futur). La différence est que le point de contrôle fait qu'on n'a pas besoin de savoir quelles sont les modifications à annuler ou non. Le banc de registre tout entier est restauré, pas seulement les registres modifiés à tord. Une autre différence est qu'avec un banc de registre futur, le RFF est mis à jour à chaque cycle d'horloge, dès qu'une instruction peut enregistrer ses résultats. Le point de contrôle n'est pas mis à jour mais est pris en une fois, en un seul cycle d'horloge, et ne change plus après. En conséquence, il n'y a pas besoin de ROB pour gérer l'état du RRF.
===La complétion dans le désordre===
Les techniques précédentes remettent dans l'ordre les écritures dans les registres, afin de gérer les branchements et exceptions. Mais elles s'appliquent sur toutes les instructions, même en absence de branchements. Mais diverses optimisations permettent de contourner ces techniques ou de les désactiver dans des conditions bien précises, pour gagner en performance, tout en garantissant que cela n'ait pas de conséquences sur l'exécution du programme. Il s'agit de techniques dites de '''complétion dans le désordre'''.
En général, elles s'appliquent en absence de branchements ou quand le processeur sait qu'aucune exception ne peut survenir. La remise en ordre des écritures est alors mise en pause et les écritures se font dans le désordre. Les écritures sont faites en avance, alors que des instructions précédentes ne sont pas terminées. Par valider des écritures en avance, on veut parler de mettre à jour les bancs de registre, qu'il s'agisse du ''retirement register file'', du point de contrôle, ou toute autre structure des techniques précédentes. Rappelons que le ''scoreboard'' ou l'unité d'émission s'arrange pour l'exécution dans le désordre ne change pas le comportement du programme exécuté.
Un exemple est celui du processeur ARM Cortex 73, qui dispose d'un tel mécanisme. Il s’applique en absence de branchements et peut être vu comme une amélioration des lectures non-bloquantes. Le mécanisme s'active quand une lecture est bloquée par un défaut de cache ou toute autre situation. Le processeur implémente l'exécution dans le désordre, ce qui fait qu'il exécute les instructions qui suivent une lecture bloquée, à condition qu'elles ne dépendent pas de la lecture. L'idée est alors de laisser ces instructions écrire dans le banc de registre, alors que la lecture précédente est encore en attente. Il faut cependant que la lecture soit arrivée à un certain état d'avancement pour que le processeur autorise ces écritures : il faut garantir que la lecture ne déclenchera pas un défaut de page ou toute autre exception matérielle. Mais une fois que le processeur sait que la situation est OK, il autorise les instructions suivant la lecture enregistrer leurs résultats pour de bon.
Le processeur ARM Cortex 73 en question ne dispose apparemment pas de ROB, mais il doit certainement avoir une structure similaire pour garantir l'ordre des écritures quand un branchement est présent. L'avantage de la technique est qu'elle permet à certaines instructions de finir en avance, ce qui libère de la place dans le ROB, le tampon d'historique, ou toute autre structure matérielle qui met en attente les écritures. Elles permet d'avoir de meilleures performances sans augmenter la taille de ces structures, ou bien d'obtenir des performances similaires à cout en circuits réduit. Le CPU ARM Cortex 73 a un budget en transistor assez restreint, ce qui fait que cette optimisation prend tout son sens sur ce CPU.
==La fréquence des avals des pipelines multicycle==
L'usage d'un pipeline complexe , qui sépare amont et avals, permet de nombreuses optimisations. Par exemple, il est possible de faire fonctionner certains avals à une fréquence supérieure aux autres. Typiquement, on peut faire fonctionner l'unité de calcul flottante à une fréquence inférieure des unités de calcul entières, afin d'économiser un peu d'énergie. Ou encore, certaines ALU peuvent fonctionner à une fréquence double de celle du processeur, afin de gagner en performance. Voyons un petit peu quelles sont ces optimisations.
===Le ''clock gating'' des avals inutilisés===
Afin de réduire la consommation d'énergie du processeur, les avals inutilisés peuvent être désactivés, mis en veille. La technique du ''clock gating'' vue dans le chapitre sur la consommation électrique des circuits, coupe le signal d'horloge pour les unités de calcul inutilisées. Mais il est aussi possible de couper l'alimentation, ce qui porte le nom de ''power gating''.
Les processeurs avec un pipeline simple bloquent dès qu'un défaut de cache est rencontré, à savoir qu'ils émettent des bulles de pipeline tant que la RAM n'a pas répondu. Lors d'un défaut de cache, il est possible de désactiver les unités de calcul et les registres en attendant que la RAM réponde. Les registres sont réactivés juste avant que la donnée n'arrive. Les gains sont d'autant plus grands que les accès mémoires sont longs, mais il faut avouer que ce n'est pas l'exemple le plus crédible. Les processeurs modernes disposent d'optimisations, comme les lectures non-bloquantes ou l'exécution dans le désordre, qui permettent d'exécuter des calculs pendant un défaut de cache.
Un autre exemple, bien plus intéressant, se base sur une observation assez intéressante : il est très rare qu'un programme entrelace des instructions flottantes et entières. En conséquence, il est possible de désactiver la FPU et le banc de registres flottants si elles sont inutilisées. Et il est aussi possible de désactiver l'ALU entière et les registres entiers pendant les instructions de calcul flottant. Les instructions flottantes étant assez longues, généralement une dizaine de cycles, voire plus, désactiver l'ALU et les registres entiers permet d'économiser pas mal d'énergie. Et vu qu'une instruction flottante vient rarement seule, l'ALU et les registres entiers sont généralement désactivés pendant une centaine/milliers de cycles d'horloge.
La désactivation des unités inutilisée est commandée par l'unité de contrôle. Une fois qu'elle a décodée l'instruction, elle sait quelles unités sont nécessaires pour exécuter l'instruction et quelles sont celles inutilisées. L'unité de contrôle a juste à envoyer des signaux de commande supplémentaires aux circuits de ''clock gating''. Les gains peuvent être substantiels. Par exemple, pour le processeur Power 5, IBM a déclaré que le ''clock gating'' lui permettait d'économiser 25% d'énergie.
===Les unités de calcul à double fréquence du Pentium 4===
Le Pentium 4 était un peu particulier dans son genre, avec une ALU à mi-chemin entre une ALU normale et une ALU bit-slicée. Il disposait de plusieurs unités de calcul sur les nombres entiers, dont l'une d'entre elle était une ALU simple qui ne gérait que les additions, les soustractions, les opérations logiques et les comparaisons. Les multiplications et décalages étaient gérés par une ALU séparée. Il y avait donc une ALU simple à côté d'une ALU complexe.
L'ALU simple était composée de deux sous-ALU de 16 bits chacune. La première envoyait le bit de retenue qu'elle a calculée à la seconde. Un point important est que l'ALU prenait deux cycles d'horloge pour faire son travail : le premier cycle calculait les 16 bits de poids faible dans la première sous-ALU, puis calculait les 16 bits de poids fort lors du second cycle (il y avait aussi un troisième cycle pour le calcul des drapeaux du registre d'état, mais passons). Le tout est appelé '''addition étagée''' (''staggered add'') dans la documentation Intel.
Et la magie était que l'unité de calcul fonctionnait à une fréquence double de celle du processeur ! Pour faire la différence entre les deux fréquences, nous parlerons de fréquence/cycle processeur et de fréquence/cycle de l'ALU. Le résultat de ce fonctionnement franchement bizarre, est que les 16 bits de poids faible étaient calculés en une moitié de cycle processeur, alors que l'opération complète prenait un cycle. L'utilité devient évidente quand on sait que l'ALU simple était utilisée pour les calculs d'adresse. L'accès à la mémoire cache intégrée au processeur a besoin des bits de poids faible de l'adresse en priorité, les bits de poids fort étant nécessaires plus tard lors de l'accès. Calculer les bits de poids faibles d'une adresse en avance permettait d'accélérer les accès au cache de quelques cycles.
La technique en question porte le nom barbare d''''ALU ''double pumped''''', dont une traduction naïve ne donne pas un terme français très parlant. L'idéal est de la parler d'ALU à double fréquence. Il peut exister des ALU à triple ou quadruple fréquence, mais ce n'est pas très utilisé. Il faut noter que certains processeurs autres que le Pentium 4 utilisent cette technique, mais nous en reparlerons quand nous serons au chapitre sur les processeurs SIMD.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les optimisations du chargement des instructions
| prevText=Les optimisations du chargement des instructions
| next=L'émission dans l'ordre des instructions
| nextText=L'émission dans l'ordre des instructions
}}
</noinclude>
5uvgnqnx1wjiqneoq1vumetfnp461ki
Fonctionnement d'un ordinateur/Le parallélisme mémoire
0
65888
774510
774147
2026-10-09T15:22:03Z
Mewtow
31375
/* Les caches pipelinés */
774510
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir les techniques de '''parallélisme mémoire''', qui permettent de gérer plusieurs accès mémoire simultanés, que ce soit au niveau du cache ou de la mémoire RAM. Par plusieurs accès mémoire simultanés, vous pensez sans doute à l'usage de cache multiports, voire de mémoires RAM multiport. C'est en effet une solution, mais ce n'est clairement pas la seule.
Par exemple, il est possible de pipeliner l'accès au cache et/ou l'accès à la mémoire, et nous verrons comment faire dans ce chapitre. Pipeliner l'accès au cache est une technique en vigueur dans les processeurs modernes, qu'ils aient ou non un pipeline dynamique. Par contre, pipeliner la mémoire est une technique ancienne, aujourd'hui peu utilisée. Elle était utilisée sur des très vieux ordinateurs, qui avaient un pipeline mais pas de mémoire cache. De nos jours, les mémoires SDRAM et DDR ne sont pas adaptées pour ça, il est très difficile de les pipeliner correctement.
Le parallélisme mémoire est utile aussi bien sur des processeurs à émission dans l'ordre que dans le désordre. Par exemple, un processeur ''in-order'' avec des lectures non-bloquantes peut en profiter. Il peut techniquement lancer une seconde lecture, après avoir rencontré une première lecture non-bloquante. En général, le nombre de lectures consécutives est cependant limité à moins d'une dizaine. À l'opposé, les processeurs sans lectures non-bloquantes profitent pas du parallélisme mémoire. À la rigueur, ils peuvent en profiter s'ils intègrent des techniques de préchargement. Pendant que le processeur exécute une lecture/écriture, il peut précharger une autre donnée en parallèle. Inutile de dire que sans pipeline processeur, le parallélisme mémoire ne sert pas à grand-chose.
==Le ''line fill buffer'' et les techniques associées==
Lors d'un défaut de cache, le processeur doit attendre que toute la ligne de cache soit chargée avant d'être utilisable. Or, la taille d'une ligne de cache est supérieure à la largeur du bus mémoire, ce qui fait qu'une ligne de cache est chargée en plusieurs fois, morceaux par morceaux, mot mémoire par mot mémoire. Le chargement peut se faire directement dans le cache, mais ce n'est pas une solution très pratique. À la place, beaucoup de processeurs ajoutent une mémoire tampon entre la RAM et le cache, appelée le '''tampon de remplissage de ligne''' (''line-fill buffer'') dans les processeurs modernes. Lors d'un défaut de cache, le processeur charge la donnée de la RAM dans le tampon de remplissage de ligne, mot mémoire par mot mémoire. Une fois plein, le tampon de remplissage de ligne est recopié dans la ligne de cache.
Le tampon de remplissage de ligne contient une ligne de cache, si ce n'est que certains bits de contrôle ne sont pas présents. Le tampon de remplissage de ligne contient un bit de validité pour chaque mot mémoire de la ligne de cache, qui indiquent si le mot mémoire a été chargé. Par exemple, prenons un processeur 64 bits, qui gère donc des mots mémoire de 8 octets, avec des lignes de cache 256 octets/32 mots mémoire. Le tampon de remplissage de ligne contiendra 32 bits de validité. Si le processeur a chargé les 6 premiers mots mémoire, les 6 premiers bits de validité seront mis à 1, les autres seront encore à zéro. Il faut noter que la disponibilité de la ligne complète se détermine assez facilement en faisant un ET logique entre tous les bits de validité. Le processeur sait ainsi quand la ligne de cache est disponible entièrement et donc quand la transférer dans le cache.
La même chose existe avec une hiérarchie de cache, sauf que l'on trouve une mémoire tampon entre chaque niveau de cache. Il y a un tampon de remplissage de ligne entre le cache L1 et le cache L2, entre le cache L2 et le L3, etc. Si le cache ne gère qu'un seul défaut de cache à la fois, le ''fill line buffer'' est une mémoire très simple, qui ne mémorise qu'une seule ligne de cache. Et cela vaut aussi bien pour un cache bloquant que non-bloquant. Mais sur les caches capables de gérer plusieurs défauts simultanés, le tampon de remplissage de ligne est une mémoire de type FIFO ou LIFO, capable de mémoriser plusieurs lignes de cache.
Notons que le tampon de remplissage de ligne est très utile pour implémenter certaines techniques, par exemple le contournement du cache. Nous avions vu dans le chapitre sur le cache que certains accès mémoire doivent contourner le cache, pour des raisons de cohérence des caches. C'est notamment nécessaire pour accéder aux périphériques, mais c'est aussi utile pour des raisons de performances dans des cas très spécifiques. Les accès qui contournent le cache se font directement dans le tampon de remplissage de ligne : le processeur écrit ou lit les données depuis ce tampon de remplissage de ligne, sans accéder au cache.
===L'''early restart'' et le ''critical word load''===
La présence du ''line buffer'' permet une optimisation assez intéressante, qui permet de réduire la latence des défauts de cache. L'optimisation consiste à lire un mot mémoire dans le tampon de remplissage de ligne, même si la ligne de cache complète n'a pas encore été chargée. Il existe deux manières de faire cela, qui portent les noms d'''early restart'' et de ''critical word load''. La première est la version la plus simple, la seconde est plus complexe mais plus performante.
Avec la technique d''''''early restart''''', la ligne de cache est chargée normalement, en partant de son premier mot mémoire. Dès que le mot mémoire lu/écrit par le processeur est copié dans le ''line fill buffer'', il est envoyé au processeur immédiatement. Illustrons le tout par un exemple, où une ligne de cache fait 16 mots mémoire. Le processeur effectue une lecture, qui lit le 5ème mot mémoire. Sans ''early restart'', le processeur doit charger les 16 mots mémoire avant de faire la lecture dans le cache. Avec ''early restart'', le processeur reçoit la donnée dès que le 5ème mot mémoire est disponible. Le processeur doit attendre que les 4 premiers mots mémoire soient chargés, puis le 5ème mot mémoire arrive et est envoyé directement au processeur, il est lu directement depuis le ''line fill buffer''. Les 11 mots mémoire suivants sont ensuite chargés dans le cache pendant que le processeur fait des calculs dans son coin.
Le ''critical word load'' est une optimisation de la technique précédente où le chargement de la ligne de cache commence directement à la donnée demandée par le processeur. Pour reprendre l'exemple précédent, où le processeur demande le 5ème mot mémoire sur 16, le ''critical word load'' charge le 5ème mot mémoire en premier et l'envoie au processeur, ce qui fait qu'il est chargé très rapidement. Pas besoin d'attendre que le processeur charge les 4 mots mémoire précédents comme avec l'''early restart''. Dans le détail, le processeur charge le 5ème mot mémoire en premier, puis charge les 11 suivants, et termine par les 4 mots mémoire du début. En clair, le ''critical word load'' commence par charger le mot mémoire lu, puis les blocs suivants, avant de revenir au début du bloc pour charger les blocs restants. Ainsi, la donnée demandée par le processeur sera la première disponible.
Pour cela, l'organisation du tampon de remplissage de ligne est modifiée de manière à rendre cela possible. Il a une taille égale à une ligne de cache complète, qui contient elle-même plusieurs mots mémoire. Dans le ''line-fill buffer'', chaque mot mémoire est stocké avec un tag, qui indique l'adresse du mot mémoire stocké dans le ''line-fill buffer''. Le ''line-fill buffer'' est donc un cache un peu particulier, qui fonctionne comme un cache du point de vue du processeur, comme une mémoire FIFO pour les transferts avec le cache. Ainsi, un processeur qui veut lire dans le cache après un défaut peut accéder à la donnée directement depuis le tampon de remplissage de ligne, alors que la ligne de cache n'a pas encore été totalement recopiée en mémoire.
==Les caches pipelinés==
Il est possible de pipeliner l'accès au cache, ce qui demande juste de rajouter des registres au bon endroits et quelques circuits de contrôle. Pipeliner l'accès au cache est une technique en vigueur dans les processeurs modernes, même ceux avec un pipeline dynamique. Elle a été introduite, pour les processeurs x86, sur les microarchitectures K6 d'AMD et P6 d'Intel (Pentium 2 et 3). Et l'implémentation peut se faire de plusieurs manières différentes. Il faut dire que découper une mémoire cache en plusieurs étapes peut se faire de plusieurs manière. Commençons par la plus simple.
Avec un cache pipeliné, l'accès au cache ne se fait pas en un seul cycle, mais en plusieurs. Cependant, on peut lancer un nouvel accès au cache à chaque cycle d'horloge, comme un processeur pipeliné lance une nouvelle instruction à chaque cycle. L'implémentation est assez simple : il suffit d'ajouter des registres dans le cache. Pour cela, on profite que le cache est composé de plusieurs composants séparés, qui échangent des données dans un ordre bien précis, d'un composant à un autre. De plus, le trajet des informations dans un cache est linéaire, ce qui les rend parfois pour l'usage d'un pipeline.
===Le ''pipelining'' des caches ''direct-mapped''===
Voici ce qui se passe avec un cache directement adressé. Pour rappel ce genre de cache est conçu en combinant une mémoire RAM, généralement une SRAM, avec quelques circuits de comparaison et un MUX. L'accès au cache se fait globalement en deux étapes : la première lit la donnée et le ''tag'' dans la SRAM, et on utilise les deux dans une seconde étape. Nous avions vu il y a quelques chapitre comment pipeliner des mémoires, dans le chapitre sur les mémoires évoluées. Et bien ces méthodes peuvent s'utiliser pour la mémoire RAM interne au cache !L'idée est d'insérer un registre entre la sortie de la RAM et la suite du cache, pour en faire un cache pipéliné, à deux étages. Le premier étage lit dans la SRAM, le second fait le reste. L'implémentation sur les caches associatifs à plusieurs voies est globalement la même à quelques détails près.
[[File:Cache directement adressé pipeliné - deux étages.png|centre|vignette|upright=2|Cache directement adressé pipeliné - deux étages]]
Avec le circuit précédent, il est possible d'aller plus loin, cette fois en pipelinant l'accès à la mémoire RAM interne au cache. Pour comprendre comment, rappelons qu'une mémoire SRAM est composée d'un plan mémoire et d'un décodeur. L'accès à la mémoire demande d'abord que le décodeur fasse son travail pour sélectionner la case mémoire adéquate, puis ensuite la lecture ou écriture a lieu dans cette case mémoire. L'accès se fait donc en deux étapes successives séparées, on a juste à mettre un registre entre les deux. Il suffit donc de mettre un gros registre entre le décodeur et le plan mémoire.
[[File:Cache directement adressé pipeliné - trois étages.png|centre|vignette|upright=2|Cache directement adressé pipeliné - trois étages]]
Et on peut aller encore plus loin en découpant le décodeur en deux circuits séparés. En effet, rappelez-vous le chapitre sur les circuits de sélection : nous avions vu qu'il est possible de créer des décodeurs en assemblant des décodeurs plus petits, contrôlés par un circuit de prédécodage. Et bien on peut encore une ajouter un registre entre ce circuit de prédécodage et les petits décodeurs.
: Théoriquement, toute l'adresse est fournie d'un seul coup au cache, la quasi-totalité des processeurs présentent l'adresse complète à un cache pipeliné. Mais le Pentium 4 fait autrement. Il faut noter que les premiers étages manipulent l'indice dans la SRAM, qui est dans les bits de poids faible, alors que les étages ultérieurs manipulent le ''tag'' qui est dans les bits de poids fort. Les concepteurs du Pentium 4 ont alors décidé de présenter les bits de poids faible lors du premier cycle d'accès au cache, puis ceux de poids fort au second cycle. Pour cela, l'ALU fonctionnait à une fréquence double de celle du processeur, tout comme le cache L1. Il n'y avait pas de pipeline proprement dit, mais cela réduisait grandement la latence d'accès au cache.
===Le ''pipelining'' des caches splittés/sériels===
Il est aussi possible de pipeliner un cache dits splittés, aussi appelés à accès sériel. Pour rappel, les caches à accès sériel vérifient si il y a succès ou défaut de cache, avant d'accéder aux lignes de cache en cas de succès. Ils font donc différemment des autres caches, qui accèdent à une ligne de cache, avant de déterminer s'il y a succès ou défaut en lisant le tag de la ligne de cache. Les caches sériels disposent de deux SRAM : une pour les tags des lignes de cache et une pour les données. Ils accèdent à la SRAM pour les tags, avant d’accéder à la SRAM des données en cas de succès de cache. Vu que l'accès se fait en deux étapes, une vérification des tags suivie de la lecture/écriture des données, il est facile à pipeliner.
Pipeliner le cache permet de régler le problème des accès au cache L1, et elle est tout le temps utilisé sur les processeurs modernes. Mais que faire en cas de défaut de cache ?
==Les caches non bloquants==
Un '''cache bloquant''' est un cache auquel le processeur ne peut pas accéder pendant un défaut de cache. Il faut attendre que la lecture ou écriture en RAM soit terminée avant de pouvoir utiliser de nouveau le cache. Un '''cache non bloquant''' n'a pas ce problème : on peut l'utiliser pendant un défaut de cache. Les caches non bloquants permettent de démarrer une nouvelle lecture ou écriture alors qu'une autre est en cours, ce qui permet d'exécuter plusieurs lectures ou écritures en même temps.
===Les ''Miss Handling Status Registers''===
Lors d'un défaut de cache, la mémoire RAM est consultée pendant le défaut de cache, mais le cache est inutilisé. Un défaut de cache n'utilise pas le cache, ce n'est qu'un accès en mémoire RAM, sur le bus mémoire (ou un accès aux niveaux de cache inférieurs, peu importe). Le cache en lui-même est laissé libre, rien n’empêche d'y accéder, il est en réalité intrinsèquement non-bloquant. Les caches, bloquants comme non-bloquants, sont en réalité composés d'une mémoire cache proprement dite, entourée de circuits qui servent d'interface entre le processeur et le cache lui-même. Et parmi les circuits tout autour, certains gèrent l'accès au cache lors d'un défaut de cache. Ils sont regroupés sous le terme de '''''Miss Handling Architecture''''' (MHA). La différence entre un cache bloquant et un cache non-bloquant est en réalité liée à l'implémentation de la MHA.
Les caches bloquants coupent volontairement l'accès au cache lors d'un défaut, car les défauts sont plus simples à gérer ainsi. Le défaut de cache rapatrie une donnée depuis la RAM, qui sera écrite dans le cache. Et il ne faut pas qu'une tentative d'accès à cette donnée ait lieu avant qu'elle ne soit chargée. Mais ce blocage est général et touche tout le cache, alors que seule une ligne de cache est concernée par le défaut de cache.
L'idée derrière un cache non-bloquant est que seule la ligne de cache est bloquée, mais les autres sont accessibles. L'idée est alors de mémoriser les lignes de cache concernées par le défaut de cache, afin d'en bloquer l'accès. À chaque accès, on vérifie si la ligne de cache est déjà réservée par un défaut de cache. La lecture/écriture est alors bloquée si c'est le cas, mais elle accepte les accès sinon. Pour cela, la ''Miss Handling Architecture'' contient des registres qui mémorisent des informations sur les défauts de cache en cours. Ils portent le nom de '''''miss status handling registers''''', que j’appellerais dorénavant MSHR, qui sont aussi appelés des '''''miss buffer'''''.
Le contenu des MSHR varie beaucoup suivant le processeur, mais ces derniers stockent au minimum les informations suivantes :
* Le numéro de la ligne de cache dans laquelle les données sont chargées.
* Un bit de validité qui indique si le MSHR est vide ou pas, qui est mis à 0 quand le défaut de cache est résolu.
* Un ou plusieurs champs de lecture/écriture, qui contiennent des informations sur la lecture/écriture.
** Pour une lecture, elle contient des informations sur la destination de la donnée, à savoir qui prévenir quand le défaut de cache est terminé. C'est parfois un nom/numéro de registre (celui dans lequel charger la donnée), mais c'est souvent le numéro de l'entrée dans le ''load/store queue''.
** Pour les écritures, elle contient la donnée à écrire, ou éventuellement un numéro de ''load/store queue'' où se trouve la donnée à écrire.
Il faut noter que le bus mémoire ne peut gérer qu'un seul défaut de cache à la fois. Aussi, il est intéressant de regarder ce qui se passe lorsqu'un second défaut de cache survient, pendant qu'un premier est en cours. Dans ce cas, il y a deux réponses qui correspondent à deux types de caches non-bloquants, qui portent les noms barbares de caches de type succès après défaut et défaut après défaut. Sur le premier type, il ne peut pas y avoir plusieurs défauts de cache simultanés. Dès qu'un second défaut de cache survient, le cache stoppe son activité et on ne peut plus démarrer de nouvelle lecture/écriture, tant que le premier défaut de cache n'est pas résolu. Le second type est plus souple et autorise la survenue de plusieurs défauts de cache simultanés. Du moins, jusqu'à une certaine limite, car le cache ne peut supporter qu'un nombre limité d'accès mémoires simultanés (pipelinés).
===Les accès simultanés à une même ligne de cache===
Il arrive que le processeur fasse plusieurs accès mémoire simultanés à la même ligne de cache. Si la ligne de cache en question n'a pas encore été chargée dans le cache, alors on a plusieurs défauts de cache consécutifs pour la même ligne de cache, et le cache non-bloquant doit gérer la situation. Pour la suite, il va falloir faire une petite distinction entre les défauts primaires et secondaires. Imaginons qu'un défaut de cache ait lieu et demande à charger une donnée dans la ligne de cache numéro N. Il s'agit du premier défaut impliquant cette ligne de cache précise, ce qui lui vaut le nom de '''défaut de cache primaire'''. Mais par la suite, d'autres accès mémoire à la même ligne de cache ont lieu, alors que la ligne de cache n'est pas encore disponible. Dans ce cas, il s'agit de '''défauts de cache secondaires'''.
Pour l'unité d'accès mémoire, les défauts de cache primaire et secondaire sont différents (ils prennent tous une entrée dans la ''load/store queue''). Mais pour le cache, ils ne correspondent qu'à un seul accès au cache : celui qui demande de charger la ligne de cache demandée. Les défauts de cache primaire et secondaire à la même ligne de cache se voient attribuer un MSHR unique. La gestion des défauts de cache secondaires dépend du cache non-bloquant. La solution la plus simple ne permet pas les défauts de cache secondaires. Le cache ne permet pas deux défauts de cache simultanés pour la même ligne de cache. Les autres solutions le permettent, en fusionnant des accès simultanés à la même ligne de cache en un seul au niveau des MSHR. Dans tous les cas, détecter les défauts de cache secondaires sont un problème qu'il faut détecter.
La ''Miss Handling Architecture'' doit détecter les défauts de cache secondaire. Pour cela, elle procède comme suit. Lors de chaque défaut de cache, la MHA récupère le numéro de la ligne de cache associé. Il vérifie alors chaque MSHR pour vérifier s'il contient le numéro en question. S'il n'y a aucune correspondance dans les MSHR, alors c'est signe que le défaut de cache est un défaut primaire. Mais s'il y en a une, alors c'est un défaut secondaire. Évidemment, cela signifie que lors d'un défaut de cache, le numéro de ligne de cache est envoyé à tous les MSHR, pour comparaison. Les MSHR sont donc regroupés dans une mémoire associative, une sorte de mini-cache, faciliter l'implémentation.
Lors d'un défaut de cache primaire, l'accès à un cache non-bloquant se fait comme suit : le processeur envoie une adresse au cache, accède à celui-ci, et détecte la survenue d'un défaut de cache. Il en profite alors pour attribuer une ligne de cache dans laquelle sera chargée ce défaut. L'attribution est très simple dans le cas des caches ''direct mapped'', ou associatifs par voie, pour lesquels l'attribution se fait assez simplement. Il mémorise alors cette information dans les MSHR, après avoir vérifié que le défaut de cache n'était pas un défaut secondaire.
Lors des accès ultérieurs à une adresse proche, censée être dans la même ligne de cache, le processeur va encore une fois rencontrer un défaut de cache. Il va alors déterminer le numéro de la ligne de cache associée à l'adresse, et comparer ce numéro avec les MSHRs. Si un MSHR contient ce numéro, c'est signe que le défaut de cache est un défaut secondaire. La MHA réagit alors différemment selon le processeur considéré. Une première solution n'autorise pas les défauts de cache secondaires. Si l'un d'entre eux survient, le processeur est gelé par un ''pipeline stall'', une bulle de pipeline. Une autre solution fusionne les défauts de cache secondaires avec le défaut de cache primaire : tout cela ne correspond qu'à un seul défaut de cache pour lui.
===Les MSHR simples===
Un cache non-bloquant à '''MSHR simple''' contient juste plusieurs MSHR qui mémorisent juste un numéro de ligne de cache, un bit de validité, le champ de lecture/écriture, et l'adresse exacte de lecture/écriture. Avec cette organisation, il est possible d'avoir plusieurs défauts de cache séparés, mais à la condition que chaque défaut accède à une ligne de cache différente. Deux accès simultanés à une même ligne de cache ne sont pas possibles, les défauts de cache secondaires ne sont pas autorisés. Ainsi, chaque défaut de cache se voit attribuer son propre MSHR, chacun contient un numéro de ligne de cache différent.
Pour comprendre pourquoi c'est impossible de gérer les défauts secondaires, il faut regarder le champ de lecture/écriture. Si on veut effectuer plusieurs écritures consécutives à la même adresse, le MSHR n'aura pas de quoi mémoriser les deux données à écrire. Il pourra mémoriser la première donnée à écrire, pas la seconde. Même chose lors d'une lecture : le champ lecture/écriture peut mémorisr la destination de la première lecture, pas de la seconde.
L'avantage est que la MHA n'a besoin que des MSHR et de quelques circuits annexes. Les autres solutions rajoutent des circuits annexes pour gérer les défauts de cache secondaires, qui utilisent beaucoup de circuits. Le cout en circuit est donc élevé, mais le gain en performance est là. Passons maintenant aux caches non-bloquants qui autorisent les défauts de cache secondaire. La solution la plus simple consiste à utiliser
===Les MSHR adressés implicitement===
Avec les '''MSHR adressés implicitement''', il est possible de fusionner plusieurs accès mémoire à une même ligne de cache, mais sous une condition très importante : ces accès lisent/écrivent des mots mémoire différents. Par exemple, imaginons qu'une ligne de cache contienne 8 mots mémoire de 64 bits. Si un premier accès mémoire lit le mot mémoire numéro 7 (dernier mot mémoire de la ligne), et le second accès le mot mémoire numéro 3, alors la fusion est possible. Mais si deux accès mémoire veulent lire/écrire le mot mémoire numéro 7, alors la fusion n'est pas possible et le processeur se bloque, un ''pipeline stall'' survient.
Un MSHR adressé implicitement est un MSHR simple, qui contient naturellement un numéro de ligne de cache (tag) et un bit de validité, sauf que le champ de lecture/écriture est dupliqué. Les différents champs lecture/écriture d'un MSHR sont regroupés dans une mémoire RAM/cache qui contient autant d'entrées qu'il y a de mot mémoire dans une ligne de cache. Chaque entrée de la table est associée à un mot mémoire de la ligne de cache et stocke des informations sur celui-ci. Une entrée mémorise, au minimum :
* Un champ de lecture/écriture qui contient soit la destination de la lecture, soit la donnée à écrire.
* Un bit R/W permettant d'interpréter correctement le champ de lecture/écriture.
* Un bit de validité pour chaque entrée de la table, qui dit si un défaut de cache antérieur accède déjà à ce mot mémoire.
La fusion de deux défauts de cache est ainsi assez simple. Un défaut de cache primaire/secondaire configure l'entrée associée au mot mémoire lu/écrit. Si un défaut secondaire ultérieur lit/écrit un mot mémoire différent (dont le bit de validité de l'entrée dans la table est à zéro), pas de conflit : il configure une autre entrée, vide. Mais s'il lit/écrit un mot mémoire pour lequel l'entrée de la table est occupée, il y a conflit, le processeur est bloqué par un ''pipeline stall'', une bulle de pipeline. Avec cette organisation, le nombre de MSHR indique combien de lignes de cache peuvent être lues en même temps depuis la mémoire. Quant au nombre d'entrées par MSHR, il détermine combien d'accès mémoires qui ne se recouvrent pas peuvent avoir lieu en même temps.
===Les MSHR adressés explicitement===
Les '''MSHR adressés explicitement''' n'ont pas les contraintes des MSHR adressés implicitement. Avec eux, il est possible d'avoir plusieurs défauts de cache pointant vers la même ligne de cache, mais aussi vers le même mot mémoire. De tels défauts de cache apparaissent sur les processeurs à exécution dans le désordre, mais sont très rares, voire inexistants, sur les processeurs ''in-order''.
L'idée est encore que chaque MSHR est associée à une table mémoire qui mémorise des entrées. Cette table n'est autre qu'une mémoire FIFO. Sauf que cette fois-ci, une entrée n'est pas associée à un mot mémoire. Une entrée est associée à un défaut de cache. Une entrée mémorise là encore la destination de la lecture ou la donnée de l'écriture suivi du bit R/W, ainsi que d'un bit de validité par entrée, mais aussi : la position du mot mémoire lu dans la ligne de cache. C'est cette dernière information qui n'était pas présente dans les MSHR adressés implicitement. De plus, le nombre d'entrée par MSHR n'est pas égal au nombre de mots mémoires dans une ligne de cache, mais peut être arbitrairement grand.
L'avantage d'un tel cache est qu'il est capable de traité les défauts secondaires concernant un même mot mémoire, et ce, car les accès à la ligne de cache concernée se produisent dans l'ordre de génération des défauts (la table mémoire est une FIFO).
===Les MSHRs inversés===
Généralement, plus on veut supporter de défauts de cache, plus le nombre de MSHR et d'entrées augmente. Mais au-delà d'un certain nombre d'entrées et de MSHR, les MSHR adressés implicitement et explicitement ont tendance à bouffer un peu trop de circuits. Utiliser une organisation un peu moins gourmande en circuits est donc une nécessité. Cette organisation plus économe se base sur des MSHR inversés.
Les MSHR inversés ne contiennent qu'une seule entrée, en quelque sorte : au lieu d’utiliser n MSHR de m entrées chacun, on va utiliser n × m MSHR inversés. La différence, c'est que plusieurs MSHR peuvent contenir un tag identique, contrairement aux MSHR adressés implicitement et explicitement. Lorsqu'un défaut de cache a lieu, chaque MSHR est vérifié. Si jamais aucun MSHR ne contient de tag identique à celui utilisé par le défaut, un MSHR vide est choisi pour stocker ce défaut, et une requête de lecture en mémoire est lancée. Dans le cas contraire, un MSHR est réservé au défaut de cache, mais la requête n'est pas lancée. Quand la donnée est disponible, les MSHR correspondant à la ligne qui vient d'être chargée vont être utilisés un par un pour résoudre les défauts de cache en attente.
===Les MSHR intégrés au cache===
Certains chercheurs ont remarqué que pendant qu'une ligne de cache est en train de subir un défaut de cache, celle-ci reste inutilisée, et son contenu est destiné à être perdu une fois le défaut de cache résolu. Ils se sont dit que, plutôt que d'utiliser des MSHR séparés, il vaudrait mieux utiliser la ligne de cache pour stocker les informations sur les défauts de cache en attente dans cette ligne de cache.
Pour éviter tout problème, il faut rajouter un bit dans les bits de contrôle de la ligne de cache, qui sert à indiquer que la ligne de cache est occupée : un défaut de cache a eu lieu dans cette ligne, et elle stocke donc des informations pour résoudre les défauts de cache.
==L'entrelacement mémoire==
Nous venons de voir comment une mémoire cache peut gérer plusieurs accès mémoire simultanés. Il se trouve que ce genre d'optimisation dépasse largement le cadre du cache. Les mémoires RAM ne sont pas en reste, elles aussi ! Il est possible de pipeliner l'accès à une mémoire RAM, en utilisant des techniques dites d''''entrelacement'''. Précisons cependant que cette optimisation n'est pas utilisée sur les mémoires SDRAM ou DDR modernes, du moins pas sans modifications majeures. Mais divers ordinateurs assez anciens, dont des superordinateurs, ont utilisé cette technique.
La technique demande d'utiliser plusieurs mémoires séparées. Sur les anciens ordinateurs, les mémoires en question sont des chips mémoire, à savoir des circuits intégrés de DRAM. Pour faire plus général, nous allons utiliser le terme de '''banques''', ou encore de bancs mémoire. La différence est qu'il existe des mémoires multi-banque, qui regroupent plusieurs banques indépendantes dans un seul boitier, dans un seul chip mémoire. En clair, de telles mémoires regroupent plusieurs sous-mémoires dans un seul circuit intégré. Il est aussi possible d'utiliser plusieurs chips mémoire séparés, chacun ayant une banque. Reste à combiner ces banques pour former un pseudo-pipeline.
===Utiliser plusieurs banques sans entrelacement===
Sans optimisation particulière, les adresses sont réparties dans les banques comme indiqué ci-dessous. Il s'agit de ce qui s'appelle un arrangement horizontal, et nous avions vu celui-ci dans le chapiutre sur les mémoires SDRAM.
[[File:Répartition des adresses sans entrelacement.png|centre|vignette|upright=1.5|Répartition des adresses sans entrelacement.]]
Avec cet arrangement, les bits de poids fort de l'adresse sont utilisées pour sélectionner la banque adéquate, et le reste de l'adresse est envoyé sur le bus d'adresse. Les adresses mémoire sont alors découpées comme suit :
{|class="wikitable"
|+ Adresse mémoire
|-
! Adresse de banque !! Adresse dans la banque
|-
| Quelques bits de poids fort || Reste de l'adresse
|}
L'entrelacement change cette répartition, afin d'accélérer les accès mémoire. L'entrelacement classique vise à améliorer les accès à des adresses mémoires consécutives, mais des formes plus évoluées d'entrelacement visent à optimiser des accès mémoire arbitraires. Voyons-les dans l'ordre, du plus simple au plus complexe.
===L'entrelacement classique===
Avec l''''entrelacement classique''', des adresses consécutives sont réparties dans des banques consécutives. Précisons que cette attribution des adresses n'implique absolument pas la mémoire virtuelle ou n'importe quel mécanisme dans le processeur. La répartition décide que telle adresse mémoire va dans telle banque, à telle adresse dans la banque. Elle est donc le fait du contrôleur mémoire, donc en dehors du processeur (les contrôleurs mémoires n’étaient pas encore intégrés dans le processeur à l'époque).
[[File:Répartition des adresses dans une mémoire interleaved.png|centre|vignette|upright=1.5|Répartition des adresses dans une mémoire interleaved.]]
L'entrelacement simple permet d'accélérer les accès mémoire qui se font à des adresses consécutives. Avec l'entrelacement, chaque accès mémoire tombe dans une banque mémoire différente, ce qui fait qu'on peut démarrer un nouvel accès mémoire à chaque cycle d'horloge. Pendant qu'une banque est occupée par un accès mémoire, on démarre le suivant dans une autre banque, et ainsi de suite. Le tout se termine soit quand on a épuisé toutes les banques libres, soit quand l'accès en rafales se termine. Pas besoin d'attendre que la mémoire ait fini sa lecture/écriture avant de démarrer la lecture/écriture suivante.
[[File:Pipemining mémoire.png|centre|vignette|upright=2.5|Pipelining mémoire]]
L'animation suivante illustre bien le processus, avec quatres banques.
[[File:Interleaving.gif|centre|vignette|upright=2.5|Pipelining mémoire]]
L'entrelacement simple utilise les bits de poids faible pour sélectionner la banque, et les bits de poids fort pour la case mémoire.
[[File:Adresse mémoire d'une mémoire entrelacée.png|centre|vignette|upright=2|Adresse mémoire d'une mémoire entrelacée]]
Précisons que le temps d'accès mémoire ne change pas beaucoup avec l'entrelacement. Par contre, on peut faire plus d'accès mémoire simultanés. On peut démarrer un accès mémoire par cycle d'horloge, mais l'accès en lui-même prend plusieurs cycles. Les mémoires à entrelacement ont donc un débit supérieur aux mémoires qui ne l'utilisent pas.
L'entrelacement classique pourrait être utilisé pour accélérer les transferts entre mémoire RAM et cache, qui se font en blocs d'adresses consécutives. Mais de nos jours, il n'est pas utilisé dans cette optique. Il faut dire qu'il est redondant avec le mode rafale des mémoires DRAM. Les deux font globalement la même chose et on voit mal comment utiliser les deux en même temps. Le mode rafale étant plus simple à implémenter, et plus léger niveau utilisation du bus de commande mémoire, il est préféré à l'entrelacement.
Par contre, l'entrelacement a eu son heure de gloire sur d'anciens processeurs qui n'avaient pas de mémoire cache, alors qu'ils disposaient d'un pipeline. L'exemple typique est celui des nombreux ''processeurs vectoriels'', que nous n'avons pas encore abordé à ce stade du cours. De tels processeurs avaient un pipeline, une unité de calcul fortement pipelinée, et parfois même de l'exécution dans le désordre et du renommage de registres !
===L'entrelacement par décalage===
L'entrelacement simple est très utile pour les accès en rafle ou équivalents. Par contre, il existe de rares situations où il n'est pas optimal. Une de ces situations est celle des accès en enjambée (en ''stride''), où l'on accède en série à des adresses sont séparées par N mots mémoires.
[[File:Accès par enjambées.png|centre|vignette|upright=2|Accès par enjambées.]]
De tels accès surviennent quand un logiciel accède à des structures de données spécifiques, à savoir des tableaux de structures, des matrices, ou d'autres structures de données dans le genre. Un cas classique est celui du parcours d'une matrice colonne par colonne. Les matrices de nombres sont mémorisées ligne par ligne, ce qui fait que parcourir une colonne demande de passer d'une ligne à la suivante et de faire des grands sauts en mémoire RAM, mais tous espacés par la même distance.
Avec l'entrelacement simple, les accès en enjambée sont moins performants que les accès à des adresses consécutives, mais cela ne fonctionne pas trop mal. Le pire cas est celui où l'on a N banques et où les données sont justement placées toutes les N adresses. Des accès consécutifs vont tous tomber dans la même banque, on ne peut plus accéder à des banques différentes en parallèle. Mais en dehors de ce cas, on voit une amélioration par rapport à la situation sans entrelacement.
Pour obtenir des performances maximales pour les accès en enjambées, il faut répartir les mots mémoires dans la mémoire autrement. L'organisation idéale est la suivante.
: Dans les explications qui vont suivre, la variable N représente le nombre de banques, qui sont numérotées de 0 à N-1.
On commence par organiser les N premières adresses comme une mémoire entrelacée simple : l'adresse 0 correspond à la banque 0, l'adresse 1 à la banque 1, etc. Pour le bloc suivant, on décale tout d'une adresse, à savoir qu'on commence à remplir les banques à partir de la seconde, non de la première. Une fois la fin du bloc atteinte, on finit de remplir le bloc en repartant du début du bloc. Le troisième bloc d'adresse subit le même traitement, sauf qu'on commence à remplir à partir de la seconde banque. Et on poursuit l’assignation des adresses en décalant d'un cran en plus à chaque bloc. Ainsi, chaque bloc verra ses adresses décalées d'un cran en plus comparé au bloc précédent. Si jamais le décalage dépasse la fin d'un bloc, alors on reprend au début.
[[File:Mémoire interleaved par décalage.png|centre|vignette|upright=1.5|Mémoire entrelacée par décalage.]]
En faisant cela, on remarque que les banques situées à N adresses d'intervalle sont différentes. Dans l'exemple du dessus, nous avons ajouté un décalage de 1 à chaque nouveau bloc à remplir. Mais on aurait tout aussi bien pu prendre un décalage de 2, 3, etc. Dans tous les cas, on obtient un entrelacement par décalage. Ce décalage est appelé le pas d'entrelacement, noté P. Le calcul de l'adresse à envoyer à la banque, ainsi que la banque à sélectionner se fait en utilisant les formules suivantes :
* adresse à envoyer à la banque = adresse totale / N ;
* numéro de la banque = (adresse + décalage) modulo N ;
* décalage = (adresse totale * P) mod N.
Avec cet entrelacement par décalage, on peut prouver que la bande passante maximale est atteinte si le nombre de banques est un nombre premier. Seulement, utiliser un nombre de banques premier peut créer des trous dans la mémoire, des mots mémoires inadressables. Malgré ce défaut, la technique a été utilisée sur quelques ordinateurs, avec l'exemple notable du superordinateur ''Burroughs Scientific Processor''. Pour éviter cela, il y a plusieurs solutions. Par exemple, on peut faire en sorte que N et la taille d'une banque soient premiers entre eux : ils ne doivent pas avoir de diviseur commun. Mais en pratique, elles n'ont pas vraiment été implémentées dans une vraie machine, et sont restées à l'état de recherche, aussi je les passe sous silence.
===L'entrelacement pseudo-aléatoire===
Une dernière méthode de répartition consiste à répartir les adresses dans les banques de manière "pseudo-aléatoire". L'idée exacte est de faire passer l'adresse dans une '''fonction de hachage''', plus ou moins complexe. Pour rappel, une fonction de hachage prend une entrée de grande taille et fournit en sortie un résultat de petite taille. Idéalement, elles doivent donner des résultats aussi différents que possible pour des entrées similaires, histoire de simuler une sorte de pseudo-aléatoire. Ici, une adresse est transformée en un numéro de banque.
Une fonction de hachage simple ne fait que permuter des bits de l'adresse pour obtenir son résultat. Elle ne fait qu'échanger des bits de place avant de couper l'adresse en deux morceaux : un pour la sélection de la banque, et un autre pour la sélection de l'adresse dans la banque. Cette permutation est fixe, et ne change pas suivant l'adresse. Des fonctions de hachage plus complexes font des XOR bit à bit entre certains bits de l'adresse.
==Les contrôleurs SDRAM/DDR avec "exécution dans le désordre"==
Après avoir vu le cas des mémoires RAM, nous allons nous concentrer sur le cas particulier des mémoires SDRAM. Les mémoires SDRAM et DDR modernes sont capables de gérer plusieurs accès simultanés à la mémoire RAM, et leurs contrôleurs mémoire en font tout autant. C'est une différence majeure avec les mémoires asynchrones FPM/EDO, qui n'acceptaient qu'un seul accès mémoire à la fois. Leur contrôleur mémoire n'acceptait qu'un seul accès mémoire à la fois, c'était un contrôleur mémoire bloquant. Les contrôleurs mémoires des SDRAM sont eux non-bloquants et peuvent encaisser une dizaine d'accès mémoire à la fois.
Peu de choses sont connues sur les contrôleurs de SDRAM/DDR modernes, les fabricants ne donnant que peu de détails dessus. Les rares simulateurs qui tentent de décrire leur fonctionnement, comme DRAM SIM I et II, sont particulièrement simples et ne vont pas dans le détail. Néanmoins, le peu qu'on sait est tout de même instructif.
===Rappel sur les SDRAM : tampon de ligne et commandes===
Les mémoires SDRAM sont des mémoires à tampon de ligne. Elles font un accès mémoire en deux étapes. La première étape recopie une ligne de N * 64 bits dans un tampon interne à la SDRAM. La seconde étape sélectionne une colonne, à savoir une donnée de 64 bits, qui est soit envoyée sur le bus de données pour une lecture, soit modifiée par une écriture.
[[File:Mémoire à tampon de ligne.png|centre|vignette|upright=2|Mémoire à tampon de ligne]]
Les accès mémoire sont traduits par un séquenceur mémoire en une série de commandes mémoires, qui sont séparées par des délais mémoire de quelques cycles d'horloge. Les délais sont très précis, et sont à respecter à la lettre. Une lecture ou une écriture se fait en maximum trois commandes : une commande PRECHARGE qui ferme la ligne précédemment utilisée, une commande ACT qui précise l'adresse de la ligne, et une commande READ ou WRITE qui précise l'adresse de la colonne et éventuellement la donnée à écrire.
: Pour être plus précis, la commande PRECHARGE précharge les lignes de bits du plan mémoire à une tension particulière, ce qui les vide de leur contenu. Mais c'est un détail sans importance pour ce qui va suivre.
Les SDRAM permettent de se passer de la première étape si des accès consécutifs se font dans la même ligne. Une fois activée, la ligne reste ouverte et on peut accéder plusieurs fois de suite dedans. On a alors juste à préciser l'adresse de colonne dedans. Si un accès mémoire accède à une ligne déjà activée, on dit que c'est un '''succès de page'''. Si ce n'est pas le cas, on doit fermer la ligne courante, rouvrir la ligne voulue, et préciser la colonne. C'est alors un '''défaut de page'''.
{|class="wikitable"
|-
! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 !! Cycle 12 !! Cycle 13
|-
! Bus de commande/adresse
| bgcolor="#FFA0FF" | PRECHARGE || bgcolor="#FFA0FF" | ACT || || bgcolor="#A0FFFF" | READ (1) || || bgcolor="#A0FFFF" | READ (2) || bgcolor="#A0FFFF" | READ (3) || || || bgcolor="#A0FFFF" | READ (4) || bgcolor="#A0FFFF" | READ (5) || ||
|-
! Bus de données
| || || || || || bgcolor="#A0FFFF" | Donnée READ 1 || ||bgcolor="#A0FFFF" | Donnée READ 2
| bgcolor="#A0FFFF" | Donnée READ 3 || || || bgcolor="#A0FFFF" | Donnée READ 4 || bgcolor="#A0FFFF" | Donnée READ 5
|}
Les SDRAM sont parfois capables de démarrer une commande avant que la précédente soit terminée. Mais le respect des délais mémoire est très limitant, ce qui fait qu'on ne peut pas parler de réel pipeline, comme c'est le cas sur les processeurs. Parlons plutôt de '''commandes anticipées'''.
{|class="wikitable"
|-
! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11 !! Cycle 12 !! Cycle 13
|-
! Bus de commande/adresse
| || bgcolor="#A0FFFF" | ACT || || bgcolor="#A0FFFF" | READ (1) || || || || bgcolor="#FFA0FF" | READ (2) || || || || ||
|-
! Bus de données
| || || || || || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1 || bgcolor="#A0FFFF" | READ 1
| bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 || bgcolor="#FFA0FF" | READ 2 |
|}
Un dernier point est que les chips mémoires de SDRAM sont composés de plusieurs banques, chacune étant une sorte de mini-mémoire miniature. Chaque banque a son propre décodeur, son propre tampon de ligne, ses propres multiplexeurs de colonne, sa logique de rafraichissement mémoire, etc. C'est comme si une SDRAM regroupait plusieurs mémoires séparées dans un même circuit intégré.
[[File:Arrangement vertical.jpg|centre|vignette|upright=2.5|Mémoire multi-banques.]]
===La mise en attente des accès mémoire===
Un contrôleur de SDRAM peut accepter plusieurs accès mémoire en même temps. Mais cela ne signifie pas que le contrôleur sera prêt à les traiter immédiatement. Pour éviter tout problème de disponibilité, le contrôleur met en attente les accès mémoire que le processeur lui envoie, pour les exécuter dès que possible. Les accès mémoire sont mis en attente dans une mémoire FIFO, histoire de les exécuter dans leur ordre d'arrivée, notamment pour renvoyer les lectures dans l'ordre demandé. Il y a aussi une mémoire FIFO pour les données à écrire et une FIFO pour les données lues. Cette dernière sert au cas où le cache ne soit pas disponible quand on lui envoie la donnée lue.
[[File:Module d'interface avec la mémoire.png|centre|vignette|upright=2.5|Contrôleur mémoire avec mise en attente des requêtes processeur et autres optimisations.]]
===Les lectures anticipées : une forme d'OOO mémoire===
La mise en attente des accès mémoire est une optimisation intéressante, mais pas mirifique. Elle permet surtout de ne pas bloquer le processeur si le contrôleur mémoire a du travail sur la planche. Cependant, elle peut être optimisée quand on se rend compte d'une chose : le processeur ne voit que les lectures, pas les écritures. Pour les écritures, il les envoie au contrôleur mémoire et ce dernier fait le travail demandé, le processeur n'est pas prévenu quand une écriture se termine. Mais pour les lectures, c'est différent, car il reçoit la donnée lue. En conséquence, il est possible d'optimiser les écritures sans que le processeur ne voie quoique ce soit.
Une optimisation possible est alors de changer l'ordre des accès mémoire, histoire de retarder les écritures ou au contraire de les faire en avance. La raison est que les mémoires SDRAM n'aiment pas quand on alterne lectures et écritures. Les délais mémoire, les fameux ''timings'' mémoire, sont clairs : faire une suite de lecture ou une suite d'écriture est plus performant que d'alterner entre lectures et écritures. L'idée est alors de changer l'ordre des écritures de manière à les faire en bloc, idem pour les lectures.
L’optimisation est possible, mais à condition que le processeur n'y voie que du feu. Or, vous l'avez deviné, il y a des cas où ces réorganisations ne sont pas possibles, et vous avez sans doute trouvé que la situation était familière. L'optimisation en question est une forme d'exécution dans le désordre des accès mémoire, réalisée par le contrôleur mémoire lui-même ! Et qui dit exécution dans le désordre dit : problèmes liés aux dépendances de données. Changer l'ordre des écritures n'est pas toujours possible, notamment en raison des dépendances RAW, WAR et WAW.
Pour que le processeur n'y voie que du feu, il faut respecter plusieurs critères. Premièrement, les lectures doivent être renvoyées au processeur dans l'ordre. Et quand je dis dans l'ordre, cela veut dire : dans l'ordre d'envoi des lectures au contrôleur mémoire. Les écritures aussi doivent se faire dans l'ordre, histoire qu'une série d'écriture donne le bon résultat final. Le troisième critère est qu'une lecture doit donner le résultat de la dernière écriture ''à la même adresse''. Pour le dire autrement, il faut juste détecter les dépendances RAW.
Concrètement, si on a une série de lectures et d'écritures alternées, on peut regrouper les lectures et les écritures dans deux paquets séparés, à condition qu'aucune lecture ne lise une adresse écrite par une écriture. Mais si une lecture est dépendante d'une écriture, alors la lecture doit attendre que l'écriture se termine, idem pour les lectures suivantes.
Une implémentation basique remplace la mémoire FIFO vue au-dessus, par deux mémoires FIFOs : une pour les lectures, une pour les écritures. Lorsqu'un accès mémoire arrive au séquenceur, il regarde si c'est une lecture ou une écriture et place l'accès mémoire dans la file adéquate. Le fait que ce soit des FIFOs garantit que les lectures se font dans l'ordre, idem pour les écritures. En sortie des deux FIFOs, le séquenceur mémoire détecte les dépendances RAW. Précisément, quand une lecture sort de la mémoire FIFO, il consulte la file d'attente des écritures pour voir si la lecture est dépendante d'une écriture en attente. La lecture attend si c'est le cas, et les écritures sont exécutées à la place.
Séparer lectures et écriture est une source d'optimisation. Il est par exemple possible d'utiliser le réacheminement lecture sur écriture (''Store to load forwarding''). Il s'agit d'un équivalent de l’optimisation du même nom, utilisée dans le cadre de la désambiguïsation mémoire. Imaginez qu'une lecture accède à une donnée pas encore écrite, en attente dans le contrôleur mémoire. C'est une dépendance RAW assez claire. Dans ce cas, le contrôleur mémoire peut renvoyer la donnée directement depuis ses mémoires FIFOs, sans faire d'accès mémoire en lecture.
Le contrôleur mémoire a tendance à privilégier les lectures, car celles-ci sont cruciales pour l'exécution dans le désordre. Plus elles se font tôt, plus vite le processeur recevra les données et pourra démarrer les instructions dépendantes de cette donnée. Le séquenceur a donc tendance à piocher en priorité dans la file de lecture, plutôt que dans la file d'écriture. À vrai dire, dans le cas idéal, les écritures ne sont faites que quand la file d'écriture est pleine ou quasi pleine...
Prenons l'exemple des coprocesseurs IO 81341 et 81342, qui étaient en réalité des ''chipsets'' intégrant un contrôleur SDRAM. Le ''chipset'' avait 5 ports : un pour les processeurs, un pour le pont sud (''southbridge'') et trois pour des canaux DMA. Pour le port processeur, il n'y avait pas de réordonnancement et ce port utilisait une seule mémoire FIFO. Les autres ports utilisaient l'optimisation qu'on vient de voir, et avaient vraisemblablement des files séparées pour les lectures et écritures. Le séquenceur mémoire vérifiait les dépendances mémoire de type RAW et autres.
[[File:Double file d'attente pour les lectures et écritures.png|centre|vignette|upright=2.5|Double file d'attente pour les lectures et écritures]]
===Le ré-ordonnancement des commandes mémoires===
L'optimisation précédente est peu poussée, comparé aux formes d'exécution dans le désordre que les processeurs utilisent. Et on peut se demander si une exécution dans le désordre plus poussé est possible. La réponse est oui : un contrôleur mémoire peut faire des réorganisations bien plus poussées. Par contre, il faut faire une précision très importante : le contrôleur mémoire ne remet pas les accès mémoire dans l'ordre avant de les envoyer au processeur. C'est le processeur qui remet en ordre les accès mémoire, dans sa ''load queue''. En clair : le processeur voit les accès mémoire dans le désordre, c'est lui qui les remet en ordre.
Pour cela, les requêtes mémoire sont "numérotées", à savoir qu'on leur attribue un identifiant binaire. Le contrôleur mémoire exécute les lectures/écritures dans le désordre, mais garde la trace de leur identifiant. Il sait dans quel ordre il exécute les accès mémoire et connait leur latence. Ce qui fait qu'il sait que la donnée lue à tel instant est associée à la requête mémoire numéro X. Pour les lectures, il renvoie la donnée lue avec l'identifiant. Le processeur sait alors à quelle lecture la donnée lue correspond et il se débrouille en interne pour gérer la situation.
Maintenant que ces précisions sont faites, posons cette question : dans quelles situations est-il pertinent de faire des accès mémoire dans le désordre ?
La réponse est : quand plusieurs accès à une même ligne ne sont pas consécutifs, qu'il y a des accès entre les deux. Après ré-ordonnancement, ces accès mémoire à une même ligne sont exécutés l'un à la suite de l'autre, ce qui est beaucoup plus rapide. Pour rendre le tout plus concret, voici un exemple. Imaginez que l'on sait les 3 accès mémoire suivants :
* Une lecture ligne A ;
* PRECHARGE + ACT ;
* Une écriture ligne B ;
* PRECHARGE + ACT ;
* Une lecture ligne A.
Sans réordonnancent, on doit émettre deux commandes PRECHARGE quand on passe d'une ligne à l'autre, et il faut ajouter les commandes ACT avec. Pour éviter cela, le contrôleur mémoire peut retarder l'écriture, ou au contraire avancer la seconde lecture. Le résultat est alors le suivant :
* Lecture ligne A ;
* Lecture ligne A
* PRECHARGE + ACT ;
* Une écriture ligne B.
On a donc deux accès consécutifs à la même ligne, suivi par une écriture dans une autre ligne. La technique marche parce que l'on a un mix adéquat de lectures et d'écritures. Et encore une fois, il faut éviter d'intervertir lectures et écritures à une même adresse, sous peine de problèmes. Encore une fois, les dépendances RAW posent problème !
L'implémentation est assez simple : la ou les mémoires FIFOs précédentes sont remplacées par des mémoires similaires aux ''schedulers''. Le contrôleur mémoire vérifie à chaque cycle les accès en attente, et vérifie à quelle ligne ils accèdent. Il priorise alors les accès qui tombent dans la ligne ouverte : ceux-là sont exécutés avant les autres. S'il n'y en a pas, il prend l'accès mémoire le plus ancien (ordre FIFO). Il teste aussi les dépendances mémoires RAW avant d'envoyer des commandes à la mémoire DDR/SDRAM. une telle solution est appelée l''''algorithme ''FR-FCFS''''' (''First Ready-First Come First Serve'').
===Les optimisations liées à la présence de plusieurs banques===
Les optimisations précédentes sont décuplées par la présence de plusieurs banques dans la mémoire SDRAM. Il est en effet possible de faire plusieurs accès mémoire en même temps, dans des banques différentes. Il est possible de lancer un accès mémoire dans deux banques en même temps, par exemple. La seule contrainte est que la SDRAM est limitée à 4 banques actives en même temps.
{|class="wikitable"
|+ Pipelining basique sur les SDRAM
|-
! !! Cycle 1 !! Cycle 2 !! Cycle 3 !! Cycle 4 !! Cycle 5 !! Cycle 6 !! Cycle 7 !! Cycle 8 !! Cycle 9 !! Cycle 10 !! Cycle 11
|-
! Banque Numéro 1
| || || || colspan="3" bgcolor="#FFFFA0" | Accès mémoire || colspan="3" bgcolor="#FFFFA0" | Accès mémoire || ||
|-
! Banque Numéro 2
| || colspan="3" bgcolor="#A0FFFF" | Accès mémoire || || || colspan="3" bgcolor="#A0FFFF" | Accès mémoire || ||
|-
! Banque Numéro 3
| || || || colspan="3" bgcolor="#FFA0FF" | Accès mémoire || || colspan="3" bgcolor="#FFA0FF" | Accès mémoire ||
|}
Les optimisations précédentes peuvent s'appliquer par banque. Il est par exemple possible d'utiliser une mémoire FIFO par banque. Ainsi, les accès dans une même banque se feront en série, dans l'ordre d'arrivée, mais des accès à des banques différentes se feront dans le désordre. Cela ne pose pas de problème, car les accès se font à des adresses différentes, ce qui fait qu'il n'y a pas de conflits majeurs. Il y a cependant un risque que les lectures se fassent dans le désordre si on n'y prend pas garde.
[[File:Gestion parallèle des banques.png|centre|vignette|upright=2.5|Gestion parallèle des banques.]]
Il est aussi possible d'utiliser une file séparée pour les lectures et les écritures, pour chaque banque. Les performances sont alors améliorées comparé à deux files globales pour toute la SDRAM. En idem avec la réorganisation des accès mémoire, pour regrouper les accès à une même ligne.
Un problème avec ces optimisations est qu'il est rare que des accès mémoire se fassent dans des banques séparées. Les accès mémoire tendent à se faire avec une bonne localité spatiale, ce qui fait qu'ils tombent dans une même banque. Heureusement, les contrôleurs SDRAM/DDR modernes incorporent des optimisations pour corriger ce problème. Les optimisations en question sont une forme d'entrelacement adaptée aux mémoires SDRAM. L'entrelacement naïf ne marche pas à cause de la présence du tampon de ligne. Cependant, il peut y avoir un entrelacement entre banques SDRAM. Voyons ce que ça veut dire.
L''''entrelacement de banques''' répartit deux lignes consécutives dans deux banques différentes. Pour comprendre l'idée, prenons un exemple. Imaginons une mémoire avec deux banques et 4 lignes. Imaginons qu'on parcoure/balaye la mémoire RAM en partant des adresses basses. Sans entrelacement de ligne, les accès se feront comme suit, de gauche à droite :
{|class="wikitable"
|+ Adresse mémoire
|-
! colspan="4" | Banque numéro 1 !! colspan="4" | Banque numéro 2
|-
| Ligne 1 || Ligne 2 || Ligne 3 || Ligne 4 || Ligne 1 || Ligne 2 || Ligne 3 || Ligne 4
|}
Avec l'entrelacement de banques, les accès se feront comme suit :
{|class="wikitable"
|+ Adresse mémoire
|-
! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2 !! Banque n°1 !! Banque n°2
|-
| Ligne 1 || Ligne 1 || Ligne 2 || Ligne 2 || Ligne 3 || Ligne 3 || Ligne 4 || Ligne 4
|}
L'avantage est que passer d'une ligne à la suivante est plus rapide. Pas besoin de fermer la ligne pour passer à la suivante, on ouvre directement la ligne suivante dans une banque différente, ce qui sera plus rapide. La commande PRECHARGE pourra être envoyée à la seconde banque en avance.
Pour gérer l'entrelacement, le contrôleur de SDRAM prend en entrée l'adresse envoyée par le processeur, et la découpe en plusieurs champs : un pour sélectionner la banque adéquate, un autre pour la ligne, un autre pour la colonne, un autre pour la rangée. Sans entrelacement, les adresses mémoire sont découpées comme suit :
{|class="wikitable"
|+ Adresse mémoire
|-
| Adresse de banque || Adresse de ligne || Adresse de colonne
|}
Avec l'entrelacement de banques, l'adresse est découpée comme suit.
{|class="wikitable"
|+ Adresse mémoire
|-
| Adresse de ligne || Adresse de banque || Adresse de colonne (précise à l'octet)
|}
Il est aussi possible de faire la même chose, mais avec les rangées ou en utilisant du ''dual channel'', mais je vais passer cela sous silence. Toujours est-il que les méthodes d'entrelacement sont nombreuses et chaque contrôleur mémoire fait un peu à sa sauce. Ils permettent en général de choisir entre plusieurs entrelacements. Au minimum, ils permettent de désactiver l'entrelacement, d'activer un entrelacement de ligne, et d'autres entrelacements plus complexes, dont certains sont propriétaires et tenus secrets par le fabricant.
L'entrelacement est géré juste avant le séquenceur mémoire, dans un '''circuit d'entrelacement'''. Ce dernier intervertit certains bits de l'adresse lors des accès mémoires.
[[File:Controleur mémoire d'une SDRAM avec entrelacement.png|centre|vignette|upright=2.5|Contrôleur mémoire d'une SDRAM avec entrelacement.]]
Les contrôleurs de SDRAM précédents sont assez basiques et ne représentent pas les contrôleurs les plus évolués. Ils étaient utilisés sur les anciens PC, à une époque où ils étaient encore sur la carte mère du processeur. Mais de nos jours, le contrôleur mémoire est intégré au processeur. Et il incorpore de nombreuses optimisations afin de gagner en performances. Malheureusement, ces optimisations sont elles-mêmes dépendantes des optimisations intégrées dans le processeur, ce qui fait qu'on ne peut pas en parler ici. Elles demandent que le contrôleur mémoire reçoive plusieurs accès mémoire simultanés, ce qui n'a aucun sens à ce stade du cours. Aussi, nous allons les passer sous silence pour le moment. Nous les verrons dans le chapitre sur le parallélisme mémoire.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=La désambiguïsation mémoire
| prevText=La désambiguïsation mémoire
| next=Les processeurs superscalaires in-order
| nextText=Les processeurs superscalaires in-order
}}
</noinclude>
pa1wazd8ipjlhht44zabgmtv38qbwfd
Fonctionnement d'un ordinateur/Le pipeline
0
65892
774477
762206
2026-10-09T14:08:53Z
Mewtow
31375
/* Segmenter un processeur en plusieurs étages de pipeline */
774477
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| Motorola PowerPC G4
|7
|-
|MIPS R4400
|8
|-
|Intel Itanium
|10
|-
|Intel Pentium II
|14
|-
|Intel Pentium 3
|10
|-
|Intel Pentium 4 Prescott
|31
|-
|Intel Pentium 4
|20
|-
|AMD Athlon
|12
|-
|AMD Athlon 64
|16
|-
|IBM PowerPC 970
|16
|-
|Sun UltraSPARC IV
|14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
===Le pipelining du séquenceur===
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
La seule difficulté d’implémentation est de séparer l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction entre l'étage de chargement et l'étage de décodage, qui était déjà là. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' en passant à travers des multiplexeurs, multiplexeurs qu'il faut configurer. Et bien ces multiplexeurs et leur configuration est le fait de l'étage de ''PC Update''.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de faire la même chose avec les décodeurs d'instruction. Le décodeur est pipeliné, à savoir scindé en deux ou trois circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Mais une implémentation réelle n'aurait pas de bonnes performances, pour diverses raisons. Cependant, le pipeline précédent est un bon point de départ, qu'on peut améliorer. De simples modifications permettent de simplifier grandement le pipeline. Voyons lesquelles.
===L'équilibrage du pipeline===
Idéalement, il faudrait répartir également le travail entre les étages, mais il y a toujours un étage qui est plus lent que les autres et la durée d'un cycle d'horloge devra se caler sur cet étage. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. L'étage limitant est souvent l'étage MEM, vu que les accès au cache de données sont assez lents. Les autres étages peuvent en profiter pour faire plus de travail, afin de se caler sur l'étage limitant.
Il est intéressant de fusionner deux étages consécutifs pour obtenir un étage certes plus lent, mais moins lent que l'étage limitant. Par exemple, il est possible de fusionner l'étage de lecture des opérandes avec l'étage d'exécution. Le banc de registre et l'unité de calcul sont dans l'étage d'exécution. Il s'agit là d'un exemple où l'on regroupe deux opérations qui auraient pu tenir dans un seul étage.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
Sans l'optimisation précédente, l'unité de calcul doit caler sa rapidité sur celle d'un cycle d'accès mémoire. Elle peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
fdux1tz8h22jgvm8v5uh0se1nfq2bfw
774478
774477
2026-10-09T14:10:21Z
Mewtow
31375
/* Le pipelining du séquenceur */
774478
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| Motorola PowerPC G4
|7
|-
|MIPS R4400
|8
|-
|Intel Itanium
|10
|-
|Intel Pentium II
|14
|-
|Intel Pentium 3
|10
|-
|Intel Pentium 4 Prescott
|31
|-
|Intel Pentium 4
|20
|-
|AMD Athlon
|12
|-
|AMD Athlon 64
|16
|-
|IBM PowerPC 970
|16
|-
|Sun UltraSPARC IV
|14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
La seule difficulté d’implémentation est de séparer l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction entre l'étage de chargement et l'étage de décodage, qui était déjà là. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' en passant à travers des multiplexeurs, multiplexeurs qu'il faut configurer. Et bien ces multiplexeurs et leur configuration est le fait de l'étage de ''PC Update''.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de faire la même chose avec les décodeurs d'instruction. Le décodeur est pipeliné, à savoir scindé en deux ou trois circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Mais une implémentation réelle n'aurait pas de bonnes performances, pour diverses raisons. Cependant, le pipeline précédent est un bon point de départ, qu'on peut améliorer. De simples modifications permettent de simplifier grandement le pipeline. Voyons lesquelles.
===L'équilibrage du pipeline===
Idéalement, il faudrait répartir également le travail entre les étages, mais il y a toujours un étage qui est plus lent que les autres et la durée d'un cycle d'horloge devra se caler sur cet étage. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. L'étage limitant est souvent l'étage MEM, vu que les accès au cache de données sont assez lents. Les autres étages peuvent en profiter pour faire plus de travail, afin de se caler sur l'étage limitant.
Il est intéressant de fusionner deux étages consécutifs pour obtenir un étage certes plus lent, mais moins lent que l'étage limitant. Par exemple, il est possible de fusionner l'étage de lecture des opérandes avec l'étage d'exécution. Le banc de registre et l'unité de calcul sont dans l'étage d'exécution. Il s'agit là d'un exemple où l'on regroupe deux opérations qui auraient pu tenir dans un seul étage.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
Sans l'optimisation précédente, l'unité de calcul doit caler sa rapidité sur celle d'un cycle d'accès mémoire. Elle peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
2pcusb40evvtu0l2iwa7jlwy6walg2b
774479
774478
2026-10-09T14:14:06Z
Mewtow
31375
/* Le pipelining du séquenceur */
774479
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| Motorola PowerPC G4
|7
|-
|MIPS R4400
|8
|-
|Intel Itanium
|10
|-
|Intel Pentium II
|14
|-
|Intel Pentium 3
|10
|-
|Intel Pentium 4 Prescott
|31
|-
|Intel Pentium 4
|20
|-
|AMD Athlon
|12
|-
|AMD Athlon 64
|16
|-
|IBM PowerPC 970
|16
|-
|Sun UltraSPARC IV
|14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de pipeliner les décodeurs d'instruction, à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Mais une implémentation réelle n'aurait pas de bonnes performances, pour diverses raisons. Cependant, le pipeline précédent est un bon point de départ, qu'on peut améliorer. De simples modifications permettent de simplifier grandement le pipeline. Voyons lesquelles.
===L'équilibrage du pipeline===
Idéalement, il faudrait répartir également le travail entre les étages, mais il y a toujours un étage qui est plus lent que les autres et la durée d'un cycle d'horloge devra se caler sur cet étage. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent. L'étage limitant est souvent l'étage MEM, vu que les accès au cache de données sont assez lents. Les autres étages peuvent en profiter pour faire plus de travail, afin de se caler sur l'étage limitant.
Il est intéressant de fusionner deux étages consécutifs pour obtenir un étage certes plus lent, mais moins lent que l'étage limitant. Par exemple, il est possible de fusionner l'étage de lecture des opérandes avec l'étage d'exécution. Le banc de registre et l'unité de calcul sont dans l'étage d'exécution. Il s'agit là d'un exemple où l'on regroupe deux opérations qui auraient pu tenir dans un seul étage.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
Sans l'optimisation précédente, l'unité de calcul doit caler sa rapidité sur celle d'un cycle d'accès mémoire. Elle peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
lq6if3ttqctdw2cwggv21mt6rucitur
774480
774479
2026-10-09T14:36:32Z
Mewtow
31375
774480
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| Motorola PowerPC G4
|7
|-
|MIPS R4400
|8
|-
|Intel Itanium
|10
|-
|Intel Pentium II
|14
|-
|Intel Pentium 3
|10
|-
|Intel Pentium 4 Prescott
|31
|-
|Intel Pentium 4
|20
|-
|AMD Athlon
|12
|-
|AMD Athlon 64
|16
|-
|IBM PowerPC 970
|16
|-
|Sun UltraSPARC IV
|14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès au cache est lent===
Les étages limitants sont ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
En général, on fait en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge, les autres étages peuvent en profiter pour faire plus de travail. Il est intéressant de fusionner deux étages consécutifs pour obtenir un étage certes plus lent, mais moins lent que l'étage limitant. Par exemple, il est possible de fusionner l'étage de lecture des opérandes avec l'étage d'exécution. Le banc de registre et l'unité de calcul sont dans l'étage d'exécution. Il s'agit là d'un exemple où l'on regroupe deux opérations qui auraient pu tenir dans un seul étage.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
Sans l'optimisation précédente, l'unité de calcul doit caler sa rapidité sur celle d'un cycle d'accès mémoire. Elle peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===L'équilibrage du pipeline===
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de pipeliner les décodeurs d'instruction, à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
3rqv7m9o6hqkavkmylx1nqtqn8a1rmx
774481
774480
2026-10-09T14:39:43Z
Mewtow
31375
/* Le pipeline : rien à voir avec un quelconque tuyau à pétrole ! */
774481
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès au cache est lent===
Les étages limitants sont ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
En général, on fait en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge, les autres étages peuvent en profiter pour faire plus de travail. Il est intéressant de fusionner deux étages consécutifs pour obtenir un étage certes plus lent, mais moins lent que l'étage limitant. Par exemple, il est possible de fusionner l'étage de lecture des opérandes avec l'étage d'exécution. Le banc de registre et l'unité de calcul sont dans l'étage d'exécution. Il s'agit là d'un exemple où l'on regroupe deux opérations qui auraient pu tenir dans un seul étage.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
Sans l'optimisation précédente, l'unité de calcul doit caler sa rapidité sur celle d'un cycle d'accès mémoire. Elle peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===L'équilibrage du pipeline===
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de pipeliner les décodeurs d'instruction, à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
ppoth34y0wroz1yrltir2wel7flwiwc
774482
774481
2026-10-09T14:40:48Z
Mewtow
31375
/* Les étages du pipeline */
774482
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès au cache est lent===
Les étages limitants sont ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
En général, on fait en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge, les autres étages peuvent en profiter pour faire plus de travail. Il est intéressant de fusionner deux étages consécutifs pour obtenir un étage certes plus lent, mais moins lent que l'étage limitant. Par exemple, il est possible de fusionner l'étage de lecture des opérandes avec l'étage d'exécution. Le banc de registre et l'unité de calcul sont dans l'étage d'exécution. Il s'agit là d'un exemple où l'on regroupe deux opérations qui auraient pu tenir dans un seul étage.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
Sans l'optimisation précédente, l'unité de calcul doit caler sa rapidité sur celle d'un cycle d'accès mémoire. Elle peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===L'équilibrage du pipeline===
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de pipeliner les décodeurs d'instruction, à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
nozz8fewqmazxvmnhtbwh9434sxe7qq
774483
774482
2026-10-09T14:41:12Z
Mewtow
31375
/* L'accès au cache est lent */
774483
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès au cache est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
En général, on fait en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge, les autres étages peuvent en profiter pour faire plus de travail. Il est intéressant de fusionner deux étages consécutifs pour obtenir un étage certes plus lent, mais moins lent que l'étage limitant. Par exemple, il est possible de fusionner l'étage de lecture des opérandes avec l'étage d'exécution. Le banc de registre et l'unité de calcul sont dans l'étage d'exécution. Il s'agit là d'un exemple où l'on regroupe deux opérations qui auraient pu tenir dans un seul étage.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
Sans l'optimisation précédente, l'unité de calcul doit caler sa rapidité sur celle d'un cycle d'accès mémoire. Elle peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===L'équilibrage du pipeline===
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de pipeliner les décodeurs d'instruction, à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
132sp0yxz6xvoimaff2rv64y6gta20w
774487
774483
2026-10-09T14:45:52Z
Mewtow
31375
/* L'accès au cache est lent */
774487
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès au cache est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
===L'équilibrage du pipeline===
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache". Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction.
Il est aussi possible de pipeliner les décodeurs d'instruction, à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
35993kj4rv504b5dm07xl2p2e0dihv8
774495
774487
2026-10-09T14:53:37Z
Mewtow
31375
/* L'équilibrage du pipeline */
774495
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès au cache est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Et c'est sans compter le décodage lui-même. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés.
A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
43wooiluybk9usk0ad1edmktcmmn4i8
774496
774495
2026-10-09T14:53:52Z
Mewtow
31375
/* L'accès au cache est lent */
774496
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Et c'est sans compter le décodage lui-même. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés.
A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents. Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
ip7dutoe3n83jxl35euece3v8e0v2e4
774499
774496
2026-10-09T15:01:05Z
Mewtow
31375
/* Les décodeurs sont lents sur les CPU CISC */
774499
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
La solution précédente fait en sorte de se caler sur l'étage limitant, quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
kuhn1shn7m67aat96b5vbfsfbtx071k
774501
774499
2026-10-09T15:02:14Z
Mewtow
31375
/* L'accès aux caches L1 est lent */
774501
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
adpll4ps270yhz46cscaf5gn8qjg2ne
774505
774501
2026-10-09T15:15:23Z
Mewtow
31375
/* L'accès aux caches L1 est lent */
774505
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge, même que les premiers processeurs x86, jusqu'aux micro-architectures K5 d'AMD, et à la micro-architecture P6 (Pentium 2 et 3). Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Toujours est-il que les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction. Et il est même possible de faire la même avec l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
c6vguu18sykb8y7hjwwx8txoyhdw6px
774508
774505
2026-10-09T15:19:33Z
Mewtow
31375
/* L'accès aux caches L1 est lent */
774508
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
ac49qjts8ughtltwb7eoin18wcdn86q
774509
774508
2026-10-09T15:20:17Z
Mewtow
31375
/* Les décodeurs sont lents sur les CPU CISC */
774509
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline.
Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Intuitivement, on se dit qu'il faudrait leur donner un étage chacun, et ce n'est pas une mauvaise idée. La seule subtilité est que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.. De plus, ils ne sont pas utilisés en même temps : la lecture des opérandes se fait en premier, puis les calculs/accès mémoire, et enfin l'enregistrement du résultat dans les registres. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
===La mise en parallèle des unités de calcul===
Dans la section précédente, on a vu que certains étages sont « facultatifs » pour certaines instructions. Les instructions concernées doivent passer par ces étages, mais ceux-ci ne doivent rien faire. Pourrait-on améliorer le pipeline en rajoutant des interconnexions, pour éliminer des étages facultatifs ? Pas complétement, mais quelques optimisations peuvent aider.
Pour cela, une idée est de déplacer l'unité mémoire dans l'étage d'exécution, afin qu'elle travaille en parallèle de l'unité de calcul. Une instruction arithmétique passe par l'ALU entière, mais n'utilise pas l'unité mémoire. Par contre, une instruction mémoire passe dans l'unité d'accès mémoire et laisse l'ALU entière tranquille. Le chemin de données est alors scindé en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
s8ceru5un6105sbk6ay6303i0xvq4l3
774521
774509
2026-10-09T16:24:51Z
Mewtow
31375
774521
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un mêmé étage de pipeline, qu'on appellera l'''étage d'execution''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés. Le résultat est une organisation en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Il faut noter que cette optimisation ne s'applique pas qu'à l'unité mémoire. Il est possible de mettre plusieurs unités en parallèle dans le même étage d'exécution. Par exemple, de nombreux processeurs ont une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L'unité est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Avec trois unités en parallèle, il est possible de les utiliser en même temps. Par exemple, pour exécuter un calcul dans l'ALU entière, pendant que l'unité mémoire fait une lecture/écriture. Une possibilité vient des modes d'adressage à post-incrément. Ils demandent d'incrémenter un pointeur en parallèle d'un accès mémoire. L'incrémentation est faite dans l'ALU pendant que l'accès mémoire a lieu. Pour l'unité de branchement, une possibilité est d'optimiser les branchements relatifs : l'unité pour les branchements fait une comparaison, pendant que l'ALU entière fait l'addition du décalage au ''program counter''.
Le dernier étage enregistre dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et il est facultatif. En effet, l'instruction STORE n'enregistre pas de données dans les registres, contrairement à l'instruction LOAD. Pareil avec les branchements : ils n'enregistrent rien dans les registres et ne font que transmettre leur résultat au séquenceur et au ''program counter''. Le dernier étage est utilisé par les instructions arithmétiques et l'instruction LOAD, mais il ne fait rien pour une instruction STORE ou un branchement.
L'avantage est que l'étage d'exécution est toujours utilisé par n'importe quelle instruction. À la rigueur, les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire, mais ce n'est pas le cas si l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. On a donc éliminé un étage facultatif. Mais n'allez pas croire qu'il s'agit d'une optimisation miracle, elle a quelques défauts qu'on va voir immédiatement.
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Une autre solution fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
Cependant, il faut tenir compte d'un détail : il y a des instructions qui n'utilisent pas l'unité de calcul, d'autres qui n'utilisent pas l'unité mémoire. Par exemple, sur un processeur RISC, les instructions arithmétiques n'utilisent pas l'unité mémoire. Et c'est pareil pour les branchements, les copies entre registres, etc. L'étage MEM est donc facultatif, seules les instructions LOAD et STORE l'utilisent. Les autres instructions doivent traverser cet étage, mais l'étage ne doit rien faire. L'étage MEM doit juste propager le résultat de l'ALU d'un registre de pipeline vers le suivant. Pour cela, il faut ajouter un multiplexeur qui choisit entre : la sortie de l'unité mémoire, le registre de pipeline précédent.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
L'unité mémoire n'est pas la seule dans ce cas. Par exemple, certaines instructions mémoire n'utilisent pas l'ALU entière. L'exemple type est l'instruction MOV qui copie un registre dans un autre. Ou encore, les instructions LOAD et STORE avec certains modes d'adressage, qui ne demandent pas de calculer des adresses. Ce genre de situation se règle en effectuant un NOP dans l'unité de calcul. Si on a besoin que l'ALU ne fasse rien, on lui demande de faire un NOP. Le pipeline devient alors le suivant :
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
ozdgyke3pzji1buc1wkuck60phk8ipc
774522
774521
2026-10-09T16:38:52Z
Mewtow
31375
/* Le pipelining du chemin de données */
774522
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* chargement d’opérandes : lecture des opérandes depuis les registres ;
* exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire ;
* enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et lALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut metrte le ''barrel shifter'' en parallèle de l'ALU etnière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, et que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques cas litigieux.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Dans ce chapitre, nous étudions des pipelines de longueur fixe, où toutes les instructions prennent le même nombre de cycles pour s’exécuter, et donc le même nombre d'étages. Or, nous venons de le voir, certaines instructions vont passer par des étages sans que celui-ci ne fasse quoique ce soit. L'étage est alors configuré en mode ''transparent'', en mode ''pass-through''. Il se contente de recopier ses entrées sur ses sorties, rien de plus.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Une autre solution fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
59j0vihqb6ctrj242oiv0i3pw4t6j0c
774523
774522
2026-10-09T16:45:25Z
Mewtow
31375
/* Le pipelining du chemin de données */
774523
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés. Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge.
Dans ce chapitre, nous étudions des pipelines de longueur fixe, où toutes les instructions prennent le même nombre de cycles pour s’exécuter, et donc le même nombre d'étages. Or, nous venons de le voir, certaines instructions vont passer par des étages sans que celui-ci ne fasse quoique ce soit. L'étage est alors configuré en mode ''transparent'', en mode ''pass-through''. Il se contente de recopier ses entrées sur ses sorties, rien de plus.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Une autre solution fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
hmedtt4nktrl7i1ppy42nc79dbmhnjd
774524
774523
2026-10-09T16:47:17Z
Mewtow
31375
/* Le pipelining du chemin de données */
774524
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Dans ce chapitre, nous étudions des pipelines de longueur fixe, où toutes les instructions prennent le même nombre de cycles pour s’exécuter, et donc le même nombre d'étages. Or, nous venons de le voir, certaines instructions vont passer par des étages sans que celui-ci ne fasse quoique ce soit. L'étage est alors configuré en mode ''transparent'', en mode ''pass-through''. Il se contente de recopier ses entrées sur ses sorties, rien de plus.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Un désavantage est l'implémentation des modes d'adressage qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire si on veut mettre en parallèle l'unité mémoire et l'ALU entière.
Une autre solution demande la coopération du jeu d'instruction. Il faut que le jeu d'instruction ne supporte pas les modes d'adressages indicés. Il peut gérer le mode d'adressage indirect à registre, absolu ou d'autres modes d'adressage qui ne font pas de calcul d'adresse. Dans ce cas, pas besoin d'ajouter une unité de calcul d'adresse, l'unité mémoire est utilisée directement. En soi, ce n'est pas un problème, mais moins pratique pour les programmeurs/compilateurs.
Une autre solution fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un avantage de ce pipeline est qu'il implémente certains modes d'adressage très facilement. Les modes d’adressage en question demandent de faire des calculs d'adresse. Par exemple, les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Les instructions mémoire avec calcul d'adresse sont alors implémentées simplement : le calcul d'adresse est réalisé dans l'unité de calcul, puis l'étage suivant s'occupe de l'accès mémoire.
Le cas des instructions LOAD dépend du mode d'adressage utilisé.
* Les modes d'adressage indicés demandent de calculer une adresse à partir d'une adresse de base et d'un indice, pour ensuite faire un accès mémoire. Le calcul d'adresse est réalisé dans l'unité de calcul entière, puis l'accès mémoire est exécuté par l'unité mémoire. Le pipeline précédent marche à merveille pour les lectures : le premier étage lit l'adresse de base et l'indice, le second calcul l'adresse, le troisième fait la lecture, le dernier enregistre la donnée lue dans les registres.
* Avec le mode d'adressage indirect à registre, l'adresse à lire est lue dans les registres, il n'y a pas de calcul d'adresse. L'unité de calcul est inutilisée, seule l'unité mémoire l'est.
* Avec le mode d'adressage absolu, l'adresse est fournie par le séquenceur, ce qui fait que l'unité de calcul n'est pas utilisée, mais le banc de registre ne l'est pas non plus ! L'étage ''Read Operands'' est donc inutile, il ne fait rien.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
j0h6qipdfrywwe0izndvulcmyi0lkmi
774525
774524
2026-10-09T16:55:34Z
Mewtow
31375
/* Le pipelining du chemin de données */
774525
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
e17c2at352j161gqphzu0z66gyqlux9
774526
774525
2026-10-09T16:56:22Z
Mewtow
31375
/* Le pipelining du chemin de données */
774526
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous deux points communs :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour résumer, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Un chapitre entier sera dédié aux pipelines qui gèrent les instructions multi-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
m94jiwk2fut3cgvcxzjygujl0ng5tct
774527
774526
2026-10-09T17:00:02Z
Mewtow
31375
/* Segmenter un processeur en plusieurs étages de pipeline */
774527
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==Segmenter un processeur en plusieurs étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous un point commun : ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Sur de tels pipelines, toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. En conséquence :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Nous appellerons ces pipelines les '''pipelines 1-cycle''', et nous les opposerons aux '''pipelines multicycles''', qui supportent les instructions multicycles nativement. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Au passage, un pipeline sans instructions multicyle n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Et ces processeurs utilisaient un pipeline 1-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
d795tgycdyyvxs27vkqzbgscq7c8l4g
774528
774527
2026-10-09T17:03:53Z
Mewtow
31375
/* Segmenter un processeur en plusieurs étages de pipeline */
774528
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Ils ont tous un point commun : ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Sur de tels pipelines, toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. En conséquence :
* Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU. Le processeur ne gère pas d'instruction "complexe" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter.
* Les accès mémoire se font aussi en un seul cycle d'horloge et précisément les accès mémoire dans la mémoire cache. Le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Nous appellerons ces pipelines les '''pipelines 1-cycle''', et nous les opposerons aux '''pipelines multicycles''', qui supportent les instructions multicycles nativement. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Au passage, un pipeline sans instructions multicyle n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Et ces processeurs utilisaient un pipeline 1-cycle.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
r8wk5ntusqkrkv8mieevzfkf03byhj0
774529
774528
2026-10-09T18:02:46Z
Mewtow
31375
/* La séparation des étages de pipeline */
774529
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions, qui prennent entre 2 et 50 cycles pour s'exécuter. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles. Mais avant d'expliquer comment ils font, voyons la différence entre pipelines fixes et dynamiques.
===Les pipelines de longueur fixe et dynamiques===
Sur les pipelines que nous avons vu précédemment, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. A l'opposé, les '''pipelines de longueur variables''' gèrent des instructions qui sont plus longues que d'autres : certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux.
Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
===Les accès mémoire et le pipeline===
Il faut préciser que les accès mémoires sont pris en compte quand on parle des trois types de pipelines, mais d'une manière quelque peu subtile. Un pipeline 1-cycle a des accès mémoires qui font 1 cycle. C'est possible si la mémoire est très rapide et le processeur peu puissant. Une autre possibilité est que c'est le cache qui doit répondre en 1 cycle d'horloge, pas la mémoire RAM. Les défauts de cache sont alors gérés à part, en prenant autant de cycles que nécessaire.
Dans le monde réel, les accès mémoire prennent plus d'un cycle, même pour les accès au cache. Un accès au cache L1 peut prendre deux à trois cycles d’horloge, une latence qui n'est pas compatible avec un pipeline 1-cycle. Les pipelines multicycles autorisent le cache à répondre en plusieurs cycles d'horloge, et les défauts de cache sont encore une fois gérés à part. Ils gérent bien le cas d'un processeur avec un cache unique.
Les processeurs modernes incorporent une hiérarchie de mémoires caches, aux latences diverses. Un accès au cache L1 peut prendre deux à trois cycles d’horloge, un accès au cache L2 prend facilement 10 cycles, et un accès en RAM une centaine. Les latences en question sont variables, ce qui ne colle pas avec un pipeline avec un nombre fixe d'étapes ! Par contre, les pipelines dynamiques gèrent des accès mémoire de longueur variable, qui prennent autant de temps que possible. Ils sont très adapté aux processeurs modernes, avec dune hiérarchie de cache qui rend le temps d'un accès mémoire imprévisible.
Toujours est-il que la gestion des accès mémoire est assez complexe avec un pipeline. Un chapitre entier sera dédié aux accès mémoires sur un pipeline.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
rrxdu3s25k9twl87sdboen2o4z8dhl3
774531
774529
2026-10-09T18:31:36Z
Mewtow
31375
/* Les instructions multicycles sur un pipeline */
774531
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===Défauts de caches, multiplications et FPU===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. L'implémentation est très simple, elle utilise assez peu de circuits. L'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue es disponible. Ce signal est alors utilisé pour ''clock gate'' le signal d'horloge, du moins celui qui arrive aux registres de pipeline.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont ''clock gate'', mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
===Les pipelines de longueur fixe et dynamiques===
Sur les pipelines que nous avons vu précédemment, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. A l'opposé, les '''pipelines de longueur variables''' gèrent des instructions qui sont plus longues que d'autres : certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux.
Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
td9e2bgz0e8c18u6is9b5ief2xg2mty
774532
774531
2026-10-09T18:39:01Z
Mewtow
31375
/* Défauts de caches, multiplications et FPU */
774532
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===Défauts de caches, multiplications et FPU===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. L'implémentation est très simple, elle utilise assez peu de circuits. L'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue es disponible. Ce signal est alors utilisé pour geler les registres du pipeline.
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sotn calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
: La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. Le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu.
===Les pipelines de longueur fixe et dynamiques===
Sur les pipelines que nous avons vu précédemment, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. A l'opposé, les '''pipelines de longueur variables''' gèrent des instructions qui sont plus longues que d'autres : certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux.
Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
bmi7dz9a9ad34wmeowwysdlyva66gav
774533
774532
2026-10-09T18:40:22Z
Mewtow
31375
/* Défauts de caches, multiplications et FPU */
774533
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===La gestion des défauts de cache===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. L'implémentation est très simple, elle utilise assez peu de circuits. L'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue es disponible. Ce signal est alors utilisé pour geler les registres du pipeline.
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sotn calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
: La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. Le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu.
===Les pipelines de longueur fixe et dynamiques===
Sur les pipelines que nous avons vu précédemment, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. A l'opposé, les '''pipelines de longueur variables''' gèrent des instructions qui sont plus longues que d'autres : certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux.
Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
65lkbmqwdif2eqeo4xin30atvkbtwcv
774534
774533
2026-10-09T18:43:38Z
Mewtow
31375
/* Les pipelines de longueur fixe et dynamiques */
774534
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===La gestion des défauts de cache===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. L'implémentation est très simple, elle utilise assez peu de circuits. L'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue es disponible. Ce signal est alors utilisé pour geler les registres du pipeline.
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sotn calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
: La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. Le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
7yau9162wmet6kciadwuni2sit56wmf
774535
774534
2026-10-09T18:45:11Z
Mewtow
31375
/* La gestion des défauts de cache */
774535
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===La gestion des défauts de cache===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue es disponible. Ce signal est alors utilisé pour geler les registres du pipeline.
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sotn calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
: La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. Le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
6g3ugwuchnukukflckrv30dmi8lu5fy
774536
774535
2026-10-09T18:56:56Z
Mewtow
31375
/* La gestion des défauts de cache */
774536
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===La gestion des défauts de cache===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline.
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sotn calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2|Implémentation d'un pipeline stall lors d'un accès mémoire]]
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. Le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Il ne s'agit alors pas d'une question d'instructions multicycles.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
easc2mxll7rni1qu4ygn2ngp4tvuqbh
774537
774536
2026-10-09T18:57:07Z
Mewtow
31375
/* La gestion des défauts de cache */
774537
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===La gestion des défauts de cache===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline.
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sotn calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. Le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Il ne s'agit alors pas d'une question d'instructions multicycles.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
n59tqn720lsz1inldo6aff7r9ve6l70
774538
774537
2026-10-09T18:57:30Z
Mewtow
31375
/* La gestion des défauts de cache */
774538
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Les instructions multicycles sur un pipeline==
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, le cache de données est assez rapide pour qu'une lecture/écriture se fasse en un cycle.
Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
===La gestion des défauts de cache===
Les pipelines précédents peuvent être adaptés pour gérer les instructions multicycles. Et à vrai dire, il y a un type d'instructions multicycles qu'ils n'ont pas le choix de gérer : les instructions mémoire ! Et là, faisons une précision : les instructions mémoire qui font ''un défaut de cache''. Si une instruction mémoire lit/écrit une donnée dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction se met à devoir attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sotn calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. Le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Il ne s'agit alors pas d'une question d'instructions multicycles.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
tk199psm3gi3cn0vuxokz3ncig12lhe
774539
774538
2026-10-09T19:08:16Z
Mewtow
31375
/* Les instructions multicycles sur un pipeline */
774539
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===Les instructions multicycles sur un pipeline===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer les instructions multicycles, le processeur utilise un mélange de blocage du pipeline et de NOPs de pipeline.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
to7q62g9mn8u80mkoxdf63ceh3oh1tj
774540
774539
2026-10-09T19:12:10Z
Mewtow
31375
/* Les instructions multicycles sur un pipeline */
774540
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===Les instructions multicycles sur un pipeline===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Pour gérer les instructions multicycles, le processeur utilise un mélange de blocage du pipeline et de NOPs de pipeline. Le décodeur gère la situation.
Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles,le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En conséquence, il gèle les étages précédents pendant plusieurs cycles.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
ev08txgkykmvorp8prapp8s870oa7v6
774541
774540
2026-10-09T19:23:09Z
Mewtow
31375
/* Les instructions multicycles sur un pipeline */
774541
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. En clair, elles restent dans l'étage d'exécution.
L'étage d'exécution est occupé pendant plusieurs cycles, ce qui implique que les étages précédents soient gelés. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. L'idée est qu'il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante. Il n'envoie pas une micro-opération par cycle dans le pipeline, il attend. Pour cela, il gère la situation avec un mélange de blocage du pipeline et de NOPs de pipeline
Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En conséquence, il gèle les étages précédents pendant plusieurs cycles. De plus, il envoie des NOPs de pipeline dans le chemin de données, sauf pour la FPU/multiplieur entier occupé.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
91qy9l1vimiuncseyjgm2ktwaqiopgj
774542
774541
2026-10-09T19:47:58Z
Mewtow
31375
/* La gestion des instructions multicycles */
774542
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. En clair, elles restent dans l'étage d'exécution.
L'étage d'exécution est occupé pendant plusieurs cycles, ce qui implique que les étages précédents soient gelés. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===Les dépendances : des sources de blocages de pipeline===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
pvscndv06vrdyt7zxcqt35c3djh116s
774543
774542
2026-10-09T19:48:16Z
Mewtow
31375
/* Les dépendances : des sources de blocages de pipeline */
774543
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. En clair, elles restent dans l'étage d'exécution.
L'étage d'exécution est occupé pendant plusieurs cycles, ce qui implique que les étages précédents soient gelés. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
gi6cz4k9y917ril5jzm5u43hrnmwprg
774544
774543
2026-10-09T19:50:28Z
Mewtow
31375
/* Le blocage du pipeline */
774544
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
a9qroyqz2hd3hh6bnoymvqpqjrbevki
774545
774544
2026-10-09T19:50:40Z
Mewtow
31375
/* Les exceptions précises */
774545
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. En clair, elles restent dans l'étage d'exécution.
L'étage d'exécution est occupé pendant plusieurs cycles, ce qui implique que les étages précédents soient gelés. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
s5ohojvv0mimhryy701ldpynrlamjmo
774546
774545
2026-10-09T19:51:09Z
Mewtow
31375
/* L'optimisation du pipeline de données */
774546
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. En clair, elles restent dans l'étage d'exécution.
L'étage d'exécution est occupé pendant plusieurs cycles, ce qui implique que les étages précédents soient gelés. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
mnnble83r593yxr9lhxxf8q73qtebf6
774547
774546
2026-10-09T19:51:20Z
Mewtow
31375
/* Les branchements, leur exécution et leur décodage */
774547
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. En clair, elles restent dans l'étage d'exécution.
L'étage d'exécution est occupé pendant plusieurs cycles, ce qui implique que les étages précédents soient gelés. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
7zd2eurmj5xv8i1l4zcma0rk9w8pz1a
774548
774547
2026-10-09T20:04:44Z
Mewtow
31375
/* La gestion des instructions multicycles */
774548
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour, ce qui implique de bloquer le pipeline. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Pour éviter cela, on doit geler le pipeline tant qu'une instruction multicycle est en cours d'exécution. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
7r0z2b3f26lgsdl2goyzrqsnxaa7lup
774549
774548
2026-10-09T20:05:07Z
Mewtow
31375
/* La gestion des instructions multicycles */
774549
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Pour éviter cela, on doit geler le pipeline tant qu'une instruction multicycle est en cours d'exécution. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. Un défaut de cache demande de geler le processeur tant que les données adéquates ne sont pas chargées dans le cache. Pour cela, l'unité mémoire peut émettre un signal qui gèle le pipeline en cas de défaut de cache. Lorsque le pipeline est gelé, les instructions stagnent dans le pipeline, elle ne passent plus à l'étage suivant et restent dans l'étage courant. Geler le pipeline est assez simple, il suffit de ''clock gate'' les registres d’interfaçage, entre autres. Les registres d’interfaçage restent bloqués tant que le signal de ''clock gate'' est maintenu, donc durant tout le défaut de cache.
Mais une autre solution se passe complètement de ce système. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
6w5plgzkfn67dc40woqe882zaw33y24
774550
774549
2026-10-09T20:06:36Z
Mewtow
31375
/* Les utilisations détournées du système d'exception précise */
774550
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Pour éviter cela, on doit geler le pipeline tant qu'une instruction multicycle est en cours d'exécution. Sauf que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline. Ils ont en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. Sachant cela, le décodeur peut gérer la situation.
Le décodeur gère la situation simplement : il détecte si l'instruction est multicycle et agit en conséquence. Lorsque le processeur décode une instructions multicycle, il envoie la micro-opération à la FPU ou au multiplieur. La micro-opération va rester dans ces circuits pendant quelques cycles, le temps de faire les calculs. Par contre, il ne peut pas envoyer de nouvelle instructions dans le pipeline, tant que le calcul n'est pas terminé. En clair, il envoie l'instruction multicycle dans le chemin de donnée et attend qu'elle soit terminée pour envoyer la suivante.
Pour mettre en attente les instructions suivantes, le décodeur gèle les étages précédents pendant plusieurs cycles. De plus, il n'envoie pas une micro-opération par cycle dans le pipeline. Le chemin de données est alimenté par des NOPs de pipeline (sauf pour la FPU/multiplieur entier occupé). Une telle situation est un car particulier de ''pipeline stall'', qui est commandé par le décodeur. Nous parlerons de '''blocage de pipeline'''.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
dkx85jzx7ac1teosn1kvonc5qjek1n4
774551
774550
2026-10-09T20:18:26Z
Mewtow
31375
/* La gestion des instructions multicycles */
774551
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles.
En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont gérés au décodage. Quand le décodeur démarre une instruction multicycle, il retarde l'exécution de l'instruction suivante. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. Elle est donc mise en attente, ce qui implique : que les étages précédents sont gelés, que le décodeur envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
ix96ap81jervairhbvfxm829jc2zfal
774552
774551
2026-10-09T20:18:39Z
Mewtow
31375
/* La gestion des défauts de cache */
774552
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles.
En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont gérés au décodage. Quand le décodeur démarre une instruction multicycle, il retarde l'exécution de l'instruction suivante. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. Elle est donc mise en attente, ce qui implique : que les étages précédents sont gelés, que le décodeur envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances entre instructions===
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
1b3978zqu0yfoz1eue6z6h12mwl39ep
774553
774552
2026-10-09T20:19:41Z
Mewtow
31375
/* La gestion des dépendances entre instructions */
774553
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles.
En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont gérés au décodage. Quand le décodeur démarre une instruction multicycle, il retarde l'exécution de l'instruction suivante. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. Elle est donc mise en attente, ce qui implique : que les étages précédents sont gelés, que le décodeur envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances de données===
Pour finir, parlons de problèmes plus subtils, que tous les pipelines ont. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
mh7veejv0zygsp5gcdox9is1ppkgnkw
774554
774553
2026-10-09T20:19:50Z
Mewtow
31375
/* Le blocage du pipeline */
774554
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles.
En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont gérés au décodage. Quand le décodeur démarre une instruction multicycle, il retarde l'exécution de l'instruction suivante. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. Elle est donc mise en attente, ce qui implique : que les étages précédents sont gelés, que le décodeur envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances de données===
Pour finir, parlons de problèmes plus subtils, que tous les pipelines ont. Les problèmes en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
bj8o65bxsf1xiyhtcc6xxlb8xwkaz6o
774555
774554
2026-10-09T20:21:36Z
Mewtow
31375
/* La gestion des dépendances de données */
774555
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne ''gèrent pas les '''instructions multicycles''''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Restons sur un pipeline simple, semblable aux pipelines vus précédemment. Pour gérer des instructions multicycles, nous allons lui ajouter un multiplieur entier et une FPU. Les instructions multicycles sont en effet le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. Aussi, on ajoute les deux circuits associés aux pipelines précédents. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles.
Mais gérer des instructions multicycles entraine l'apparition de nombreux problèmes. Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles.
En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Mais faire ainsi pose quelques problèmes. Premièrement, imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Imaginez qu'une multiplication soit suivie par une addition. L'étage d'exécution est immédiatement suivi par l'étage de ''writeback''. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible si le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont gérés au décodage. Quand le décodeur démarre une instruction multicycle, il retarde l'exécution de l'instruction suivante. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. Elle est donc mise en attente, ce qui implique : que les étages précédents sont gelés, que le décodeur envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
La mise en attente des instructions se fait dans l'unité de décodage, ou après. Il est possible de séparer le décodeur en deux circuits : un qui décode les instructions, un autre qui décide des ''pipeline stall'' et gère les NOPs de pipeline. Le second circuit est appelé l''''unité d'émission'''. Elle est parfois fusionnée avec l'unité de décodage d'instruction, mais nous allons surtout étudier le cas où elle est séparée.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle, mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
q8m30haqdd3tm4gskuhq9woblvhctqy
774556
774555
2026-10-09T20:28:46Z
Mewtow
31375
/* La gestion des instructions multicycles */
774556
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des dépendances structurelles. Et elles sont gérées en retardant l'exécution des instructions qui suivent uen instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération, pour éviter qu'une dépendance structurelle pose problème. Typiquement, quand elle émet une isntruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12 , etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle, mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
apdnk7pwfbm2se5my3m1gvcmub0ug0a
774557
774556
2026-10-09T20:29:01Z
Mewtow
31375
/* La gestion des instructions multicycles */
774557
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des dépendances structurelles. Et elles sont gérées en retardant l'exécution des instructions qui suivent uen instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération, pour éviter qu'une dépendance structurelle pose problème. Typiquement, quand elle émet une isntruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle, mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, mais on peut les résumer comme suit :
* Les '''dépendances de données''' surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire.
* Les '''dépendances structurelles''' surviennent quand deux instructions veulent utiliser le même circuit en même temps.
* les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
cvs6a1wbgvj5xtzpyowgk1zsexz3p6i
774558
774557
2026-10-09T20:31:19Z
Mewtow
31375
/* La gestion des dépendances de données */
774558
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des dépendances structurelles. Et elles sont gérées en retardant l'exécution des instructions qui suivent uen instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles. La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération, pour éviter qu'une dépendance structurelle pose problème. Typiquement, quand elle émet une isntruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle, mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions et nous en parlerons à l fin du chapitre, ainsi que dans le chapitre suivant.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
3n5ooupjlhw8bfl7azrsdhsebetl70k
774559
774558
2026-10-09T20:34:49Z
Mewtow
31375
/* La gestion des instructions multicycles */
774559
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Il s'agit là d'un cas de '''dépendance structurelle''', à savoir que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des dépendances structurelles. Et elles sont gérées en retardant l'exécution des instructions qui suivent uen instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle, mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions et nous en parlerons à l fin du chapitre, ainsi que dans le chapitre suivant.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
85ogpkz7uyk7i6gt90fxegyi7pw7ouu
774560
774559
2026-10-09T20:35:41Z
Mewtow
31375
/* La gestion des instructions multicycles */
774560
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle, mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions et nous en parlerons à l fin du chapitre, ainsi que dans le chapitre suivant.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
===Les pipelines fixes et dynamiques===
Nous venons de voir que la gestion des instructions multicycles était assez complexe. La raison à cela est une caractéristique précise des pipelines que nous avons étudiés. Sur ces pipelines, toutes les instructions ont le même nombre d'étages, donc le même nombre de cycles d'horloge. Et ce, quitte à avoir des étages inutiles pour certaines instructions. De tels pipelines sont appelés des ''pipelines de longueur fixe'', ou encore : des '''pipelines fixes'''. Difficile de gérer des instructions multicycles avec de tels pipelines.
A l'opposé, il existe des ''pipelines de longueur variables'', qui gèrent des instructions qui sont plus longues que d'autres. Certaines utilisent l'ALU durant un cycle, d'autres pendant 5 cycles, etc. De tels pipelines sont aussi appelés des '''pipelines dynamiques'''. Ils sont cependant plus compliqués à implémenter, suffisamment pour mériter un chapitre dédié. Nous ne pouvons pas en dire plus sur ces pipelines pour le moment, laissons cela pour le chapitre dédié.
Intuitivement, on se dit que les pipelines de longueur fixe ne gèrent pas les instructions multicycle, qui sont le domaine réservé des pipelines dynamiques. Mais comme nous allons le voir, c'est faux. Les pipelines fixes se classent en deux types : ceux où toutes les instructions se font en un cycle, ceux où toutes les instructions se font en plusieurs cycles. Ils sont appelés respectivement les '''pipelines 1-cycle''' et les '''pipelines multicycles'''. Nous faisons ces simplifications, car la présence d'instructions multicycles complexifie grandement le pipeline. Mais rassurez-vous, nous verrons des pipelines avec des instructions multi-cycle, dans un chapitre dédié.
Pour résumer, il y a trois types de pipelines :
* Les '''pipelines 1-cycles''' où toutes les instructions s’exécutent en un cycle.
* Les '''pipelines multicycle''' où toutes les instructions s’exécutent en plusieurs cycles, le nombre de cycles est le même pour toutes les instructions.
* Les '''pipelines dynamiques''' où toutes les instructions ont des nombres de cycles variables.
{|class="wikitable"
|-
! !! Pas d'instructions multicycles !! Support des instructions multicycles
|-
! Pipeline de longueur fixe
| Pipeline 1-cycle || Pipeline multicycle
|-
! Pipeline de longueur variable
| || Pipelines de longueur variable
|}
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
39ztad2424050vytq3w06n23ngl56pz
774561
774560
2026-10-09T20:45:54Z
Mewtow
31375
/* Les pipelines fixes et dynamiques */
774561
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle, mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Le modèle d’exécution normal suppose qu'une instruction n'est démarrée que lorsque la précédente est terminée. Et ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes, mais doit gérer le cas où deux instructions sont dépendantes. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat dans les registres avant que la seconde lise ses opérandes dans les registres. Et avec un pipeline, cette contrainte n'est pas respectée, sauf en bloquant le pipeline tant que la première instruction n'est pas terminée. Un pipeline doit détecter les cas litigieux et de bloquer le pipeline si besoin. Reste à détecter ces dépendances et cela implique de savoir ce que sont ces dépendances.
Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Les '''dépendances de contrôle''' sont liées aux branchements et aux interruptions et nous en parlerons à l fin du chapitre, ainsi que dans le chapitre suivant.
: Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Les dépendances de données sont les plus intuitives. Le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter. La seconde doit alors s'exécuter après la première. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent à la même donnée, c'est à dire au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons maintenant sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre : la première lit le résultat écrit par la seconde.
* ''Write after write'' : Deux instructions écrivent dans le même registre ou la même adresse mémoire, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres dépendances imposent que la première instruction soit terminée, avant de démarrer la seconde, au moins partiellement. Elle doit terminer son exécution, avant que la seconde soit démarrée. Et cela implique des blocages de pipeline.
Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée d'une manière ou d'une autre. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
1m0pn4skoa9wynd9q4v8hh2wocpa4uc
774565
774561
2026-10-09T21:03:51Z
Mewtow
31375
/* La gestion des dépendances de données */
774565
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle. Mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, des dépendances peuvent survenir. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat avant que la seconde le lise dans les registres. Et avec un pipeline, cette contrainte n'est respectée que si l'unité d'émission intervient. L'unité d'émission retarde alors l'instruction suivante, tant que la première n'a pas écrit son résultat dans les registres.
Un pipeline doit détecter les dépendances et de bloquer le pipeline si besoin. Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Pour les dépendances de données, le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter, qu'on a vu plus haut. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre.
* ''Write after write'' : Deux instructions écrivent dans le même registre, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres imposent que la première instruction soit terminée, avant de démarrer la seconde. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance.Et c'est à l'unité d'émission de détecter ces dépendances et de bloquer le pipeline, pour obtenir le retard adéquat.
Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée de quelques cycles. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
qz9iz1jsocnrk9x93mzi7xphkwa772e
774566
774565
2026-10-09T21:04:44Z
Mewtow
31375
/* La gestion des dépendances de données */
774566
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, tous les pipelines ont des problèmes assez peu intuitifs. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes bien précises, à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle. Mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, des dépendances peuvent survenir. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat avant que la seconde le lise dans les registres. Et avec un pipeline, cette contrainte n'est respectée que si l'unité d'émission intervient. L'unité d'émission retarde alors l'instruction suivante, tant que la première n'a pas écrit son résultat dans les registres.
Un pipeline doit détecter les dépendances et de bloquer le pipeline si besoin. Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Pour les dépendances de données, le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter, qu'on a vu plus haut. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre.
* ''Write after write'' : Deux instructions écrivent dans le même registre, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres imposent que la première instruction soit terminée, avant de démarrer la seconde. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Et c'est à l'unité d'émission de détecter ces dépendances et de bloquer le pipeline, pour obtenir le retard adéquat.
Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée de quelques cycles. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
Tout cela est bel et bon, mais il y a des dépendances qui ne peuvent ^pas être réglées en bloquant le pipeline. Et ca sera le sujet de la fin de ce chapitre, ainsi que du suivant.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
baavsk47cx4syh89yia9rrh116d7v2y
774578
774566
2026-10-10T10:59:37Z
Mewtow
31375
/* Le blocage du pipeline */
774578
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipelines doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Pour le dire autrement, ils ne gèrent pas les '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter.
Un pipeline sans instructions multicycles n'est pas si irréaliste. Les tout premiers processeurs MIPS avaient un jeu d'instruction simple, sans multiplications ni division, où toutes les instructions pouvaient s’exécuter en un cycle d'horloge. Mais ce sont des exceptions, et la plupart des pipelines gèrent les instructions multicycles.
Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Et cela entraine l'apparition de nombreux problèmes.
Un premier problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes.
Et d'autres dépendances structurelles sont possibles, bien qu'assez subtiles. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur font leur travail. Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle. Mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, des dépendances peuvent survenir. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat avant que la seconde le lise dans les registres. Et avec un pipeline, cette contrainte n'est respectée que si l'unité d'émission intervient. L'unité d'émission retarde alors l'instruction suivante, tant que la première n'a pas écrit son résultat dans les registres.
Un pipeline doit détecter les dépendances et de bloquer le pipeline si besoin. Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Pour les dépendances de données, le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter, qu'on a vu plus haut. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre.
* ''Write after write'' : Deux instructions écrivent dans le même registre, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres imposent que la première instruction soit terminée, avant de démarrer la seconde. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Et c'est à l'unité d'émission de détecter ces dépendances et de bloquer le pipeline, pour obtenir le retard adéquat.
Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée de quelques cycles. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
Tout cela est bel et bon, mais il y a des dépendances qui ne peuvent ^pas être réglées en bloquant le pipeline. Et ca sera le sujet de la fin de ce chapitre, ainsi que du suivant.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
1aooi4e9lq6izu7evbt2455umhz8adv
774579
774578
2026-10-10T11:28:03Z
Mewtow
31375
/* La gestion des instructions multicycles */
774579
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipelines doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Il s'agit d'un cas fortement irréaliste, aucun processeur avec un pipeline ne fait dans le monde réel.
Les processeurs commerciaux gèrent des '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Les instructions multicycles squattent donc l'étage d'exécution pendant plusieurs cycles.
Le support des instructions multicycles entraine l'apparition de nombreux problèmes, qui sont eux aussi résolus avec des blocages de pipeline. Intuitivement, vu que les instructions multicycles occupent l'étage d'exécution pendant plusieurs cycles, les étages précédents doivent être gelés pendant ce temps. Le pipeline devrait être bloqué pendant que les instructions multicycles utilisent l'étage d'exécution. Mais ce n'est pas exactement la réalité. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur exécutent une instruction multicycle. Cependnat, faire ainsi a des conséquences assez subtiles.
Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Et s'il s'agit là d'un exemple, il se généralise assez bien.
Un autre problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Et d'autres problèmes similaires sont possibles, bien qu'assez subtiles.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle. Mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, des dépendances peuvent survenir. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. L'instruction précédente doit avoir enregistré son résultat avant que la seconde le lise dans les registres. Et avec un pipeline, cette contrainte n'est respectée que si l'unité d'émission intervient. L'unité d'émission retarde alors l'instruction suivante, tant que la première n'a pas écrit son résultat dans les registres.
Un pipeline doit détecter les dépendances et de bloquer le pipeline si besoin. Les instructions ont des dépendances très variées, qui se classent en trois types. Nous venons de voir les '''dépendances structurelles''', qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Mais il existe aussi des '''dépendances de données''', qui surviennent quand deux instructions manipulent le même registre ou la même adresse mémoire. Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Pour les dépendances de données, le cas typique est celui où une instruction a besoin du résultat d'une instruction précédente pour s’exécuter, qu'on a vu plus haut. Mais ce ne sont sont pas les seules. Deux instructions en vol ont une dépendance de données quand elles accèdent au même registre ou la même adresse mémoire. Et c'est là qu'une petite subtilité survient, car les accès mémoire se distinguent des autres instructions.
Une première distinction sépare les dépendances de registre et les dépendances d'adresse. Les '''dépendances de registre''' surviennent quand deux instructions lisent ou écrivent le même registre. Elles concernent toutes les instructions, ou presque, que ce soit des instructions de calcul, des instructions mémoire, les branchements, et bien d'autres. Les '''dépendances d'adresse''' sont par contre spécifiques pour les instructions mémoire. De telles dépendances demandent que le pipeline puisse gérer plusieurs accès mémoire simultanés, ce qui est le cas sur certains pipelines complexes. Mais pour le moment, sachez que nous ne ne pouvons pas en parler car il nous manque quelques bases. Sachez que nous aborderons le sujet dans le chapitre sur la désambiguïsation mémoire.
Restons sur les dépendances de registres, entre deux instructions en vol qui accèdent à un même registre. Les deux accès peuvent se faire en lecture ou écriture, ce qui fait quatre possibilités :
* ''Read after read'' : Deux instructions lisent la même donnée, mais pas en même temps.
* ''Read after write'' : Une instruction a pour opérande le résultat d'une autre.
* ''Write after write'' : Deux instructions écrivent dans le même registre, dans un certain ordre.
* ''Write after read'' : Une instruction lit une donnée qui est modifiée ultérieurement par une autre instruction.
Les dépendances ''Read after read'' ne sont pas de vraies dépendances et je les présente par pur souci d'exhaustivité. Par contre, les trois autres imposent que la première instruction soit terminée, avant de démarrer la seconde. Quand on parle de "terminer" ou de "démarrer", on fait référence aux lectures et écritures effectuées par l'instruction. Voici comment sont gérées les trois types de dépendances :
* ''Read after write'' : La première instruction doit écrire son résultat avant que la seconde le lise.
* ''Write after write'' : La première instruction doit écrire son résultat avant la seconde.
* ''Write after read'' : La première doit effectuer la lecture avant que la seconde n'écrive, n'écrase la donnée lue.
Pour simplifier les explications, nous allons appeler ces trois dépendances : dépendances RAW, WAR et WAW. On comprend facilement qu'en cas de dépendance RAW/WAW/WAR, la seconde instruction doit être plus ou moins retardée suivant la dépendance. Et c'est à l'unité d'émission de détecter ces dépendances et de bloquer le pipeline, pour obtenir le retard adéquat.
Avec une dépendance RAW, la première instruction doit être totalement terminée avant que la seconde démarre. Mais pour les dépendances WAW et WAR, la seconde instruction peut démarrer avant que la première soit finie, elle doit simplement être retardée de quelques cycles. Par exemple, avec une dépendance WAR, il suffit de retarder l'écriture de la seconde instruction tant que la première n'a pas fait sa lecture. Avec une dépendance WAW, là encore, il suffit de retarder l'écriture de la seconde instruction tant que la première instruction n'est pas terminée.
Tout cela est bel et bon, mais il y a des dépendances qui ne peuvent ^pas être réglées en bloquant le pipeline. Et ca sera le sujet de la fin de ce chapitre, ainsi que du suivant.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
mnde1yg139oveaanoa7o3itubyzzon8
774583
774579
2026-10-10T11:52:41Z
Mewtow
31375
/* La gestion des dépendances de données */
774583
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipelines doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Il s'agit d'un cas fortement irréaliste, aucun processeur avec un pipeline ne fait dans le monde réel.
Les processeurs commerciaux gèrent des '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Les instructions multicycles squattent donc l'étage d'exécution pendant plusieurs cycles.
Le support des instructions multicycles entraine l'apparition de nombreux problèmes, qui sont eux aussi résolus avec des blocages de pipeline. Intuitivement, vu que les instructions multicycles occupent l'étage d'exécution pendant plusieurs cycles, les étages précédents doivent être gelés pendant ce temps. Le pipeline devrait être bloqué pendant que les instructions multicycles utilisent l'étage d'exécution. Mais ce n'est pas exactement la réalité. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur exécutent une instruction multicycle. Cependnat, faire ainsi a des conséquences assez subtiles.
Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Et s'il s'agit là d'un exemple, il se généralise assez bien.
Un autre problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Et d'autres problèmes similaires sont possibles, bien qu'assez subtiles.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle. Mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, des dépendances peuvent survenir. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. Les deux instructions ont alors une '''dépendance de donnée'''. L'instruction précédente doit avoir enregistré son résultat avant que la seconde le lise dans les registres, sans quoi le résultat de la seconde instruction est faux. Et avec un pipeline, cette contrainte n'est respectée que si l'unité d'émission intervient. L'unité d'émission retarde alors l'instruction suivante, tant que la première n'a pas écrit son résultat dans les registres.
Un pipeline doit détecter les dépendances et de bloquer le pipeline si besoin. Les instructions ont des dépendances très variées, qui se classent en trois types. Nous avons vu les '''dépendances structurelles''', précédemment dans ce chapitre, qui surviennent quand deux instructions veulent utiliser le même circuit en même temps. Les '''dépendances de données''' sont elles plus nombreuses, mais elles varient suivant le pipeline. Pour les pipelines que nous avons étudié, il n'y en a qu'un seul type : les dépendances où une instruction lit le résultat d'une instruction précédente. Nous verrons les autres types de dépendances en temps voulu.
. Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Tout cela est bel et bon, mais il y a des dépendances qui ne peuvent pas être réglées en bloquant le pipeline. Il s'agit des dépendances de contrôle, qui sont liées aux branchements, aux interruptions, et aux exceptions matérielles. Et ca sera le sujet de la fin de ce chapitre, ainsi que du suivant.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
cty8pto7dr5b31hntz61k3aunbclk4l
774584
774583
2026-10-10T11:53:30Z
Mewtow
31375
/* La gestion des dépendances de données */
774584
wikitext
text/x-wiki
Dans ce chapitre, nous allons voir la technique du '''pipeline d'instruction''', qui est utilisée sur tous les processeurs modernes. L'introduction du pipeline a eu lieu sur les premiers ''mainframes'' et les supercalculateurs, dans les années 60-70. Elle a ensuite été reprise pour les premiers processeurs RISC commerciaux. Cependant, elle s'est démocratisée aux processeurs CISC modernes. Introduire un pipeline entraine une modification profonde de la microarchitecture du processeur, ainsi que l'ajout de nombreuses optimisations qui ne seraient pas possibles dans pipeline : exécution superscalaire, exécution dans le désordre, prédiction de branchements, etc. Nous passerons une dizaine de chapitre à parler du pipeline des processeurs modernes, ainsi que toutes les techniques qui vont avec.
==Le pipeline : rien à voir avec un quelconque tuyau à pétrole !==
Pour expliquer ce qu'est un pipeline d'instructions, il faut faire un rappel sur les différentes étapes d'une instruction. Dans le chapitre sur la micro-architecture d'un processeur, on a vu qu'une instruction est exécutée en plusieurs étapes bien distinctes : le chargement, le décodage, et diverses étapes pour exécuter l'instruction. Ces dernières dépendant du processeur, de l'instruction, du mode d'adressage, etc.
Sans pipeline, ces étapes sont réalisées les unes après les autres et une instruction doit attendre que la précédente soit terminée avant de démarrer. Avec un pipeline, on exécute plusieurs instructions différentes, chacune étant à une étape différente des autres. Chaque instruction passe progressivement d'une étape à la suivante dans ce pipeline et on charge une nouvelle instruction par cycle dans le premier étage.
[[File:Pipeline chaîne de traitement.png|centre|vignette|upright=2.0|Pipeline : principe.]]
Pour comprendre ce que cela signifie, comparons l’exécution de cette instruction sans et avec pipeline. Sans pipeline, on doit attendre qu'une instruction soit finie pour exécuter la suivante. L'instruction exécute toutes ses étapes, avant que l'instruction suivante démarre à l'étape 1.
[[File:Nopipeline.png|centre|vignette|upright=2.0|Exécution de trois instructions sans pipeline.]]
Avec un pipeline, on démarre une nouvelle instruction par cycle (dans des conditions optimales).
[[File:Fivestagespipeline.svg|centre|vignette|upright=2.0|Exécution de trois instructions avec pipeline.]]
Une illustration plus intuitive est la suivante. Chaque instruction correspond à une couleur :
[[File:Pipleine-3.png|centre|vignette|upright=2.0|Pipeline à 5 étages.]]
Le pipeline contient donc plusieurs instructions, qui sont à des étapes différentes de leur exécution. Les instructions en question sont appelées des '''instructions en vol''', terme de mon cru que vous ne trouverez ''a priori'' que dans ce cours.
===Les étages du pipeline===
Mettre en oeuvre un pipeline nécessite des modifications profondes de la micro-architecture du processeur. Tout d'abord, chaque étape d'une instruction doit s'exécuter indépendamment des autres, ce qui signifie utiliser des circuits indépendants pour chaque étape. Par exemple, sur un processeur sans pipeline, l'additionneur de l'ALU peut être utilisé pour mettre à jour le ''program counter'' lors du chargement, calculer des adresses lors d'un accès mémoire, les additions, etc. Mais avec un pipeline, on ne peut pas se le permettre, chaque étape doit utiliser son propre additionneur. En général, il est impossible de réutiliser un circuit dans plusieurs étapes, comme on le fait dans certains processeurs sans pipeline. Chaque circuit dédié à une étape est appelé un '''étage du pipeline'''.
Le nombre d'étapes par instruction, et donc le nombre d'étages du pipeline, est appelé la '''profondeur du pipeline'''. Il correspond au nombre maximal théorique d'instructions en vol, au nombre d'instructions exécutées en même temps dans le pipeline. Et j'ai bien dit maximal théorique : ce maximum n'est pas toujours atteint, comme on le verra plus tard. Pour les curieux, voici les longueurs de pipeline de certains processeurs plus ou moins connus.
{|class="wikitable"
|-
! Processeur !! Longueur du pipeline
|-
| Motorola PowerPC G4 || 7
|-
| MIPS R4400 || 8
|-
| Intel Itanium || 10
|-
| Intel Pentium II || 14
|-
| Intel Pentium 3 || 10
|-
| Intel Pentium 4 ||20
|-
| Intel Pentium 4 Prescott ||31
|-
| AMD Athlon ||12
|-
| AMD Athlon 64 ||16
|-
| IBM PowerPC 970 ||16
|-
| Sun UltraSPARC IV ||14
|}
Plus haut, il a été dit que les étages du pipeline sont censés être totalement séparés, mais la réalité est beaucoup plus complexe. Dans la réalité, il existe quelques circuits qui sont partagés entre plusieurs étages, car on n'a pas le choix. Deux circuits sont notablement concernés par ce problème : le ou les bancs de registres, l'accès à la mémoire. Il est fréquent qu'un registre soit lu dans un étage et écrit dans un autre, comme on le verra plus bas. Heureusement, le banc de registre est conçu pour gérer la situation, car il a des ports de lecture séparés des ports d'écriture.
Mais on verra que d'autres exemples de ce type sont assez fréquents et qu'il faut gérer la situation. Par exemple, deux instructions peuvent accéder à la mémoire simultanément, exécuetr une micro-opération dans la même unité de calcul, lire le même registre en même temps, etc. De telles '''dépendances structurelles''' surviennent quand plusieurs instructions veulent accéder à un circuit du processeur en même temps. Les processeurs modernes incorporent de nombreux circuits capables de gérer les dépendances structurelles, qu'on abordera au fur et à mesure que les occasions se présentent.
===Les pipelines synchrones et asynchrones===
Il faut que les instructions progressent d'un étage à un autre au rythme adéquat. Pour cela, la plupart des pipelines intercalent des registres entre les étages, pour les isoler mais aussi pour synchroniser leurs échanges. Les registres en question seront appelés des '''registres de pipeline''' dans ce qui suit. Un registre de pipeline s'intercale entre deux étages consécutifs. L'étage précédant ce registre écrit son résultat dedans, l'étage suivant lit son contenu quand c'est son tour, généralement au cycle d'horloge suivant.
La présence de registres aide grandement à implémenter de quoi commander le pipeline. Les transferts entre étages du pipeline peuvent être synchronisés par une horloge, ou de manière asynchrone.
[[File:Pipeline sync-async.svg|centre|vignette|upright=1.5|Pipeline synchrone et asynchrone.]]
Si le pipeline est synchronisé sur l'horloge du processeur, on parle de '''pipeline synchrone'''. Chaque étage met un cycle d'horloge pour effectuer son travail, à savoir, lire le contenu du registre qui le relie à l'étape précédente et déduire le résultat à écrire dans le registre suivant. Ce sont ces pipelines que l'on trouve dans les processeurs Intel et AMD les plus récents.
[[File:Pipeline buffered synchrone.png|centre|vignette|upright=2.0|Pipeline buffered synchrone]]
Sur d'autres pipelines, il n'y a pas d'horloge pour synchroniser les transferts entre étages, qui se font via un « bus » asynchrone. On parle de '''pipeline asynchrone'''. La synchronisation des échanges entre deux étages se fait grâce à un signal de requête et un signal acquittement. Le signal de requête REQ indique que l'étage précédent a terminé son travail et qu'on peut lire son résultat. Le signal d'acquittement signifie que l'étage destinataire a pris en compte la donnée transmise. Les signaux de commande de ces pipelines asynchrones peuvent se créer facilement avec des portes C.
[[File:Micropipeline-structure.svg|centre|vignette|upright=2.0|Micropipeline-structure]]
Les pipelines n'utilisant pas de registres sont assez rares, mais ils existent, ce sont les ''pipelines à vague''. Ils se contentent d'enchainer des étages les uns à la suite des autres, et laissent les données se propager librement d'un étage à l'autre. Ils jouent sur un timing très précis pour propager les données entre étages au bon timing, de manière à ce que le tout fonctionne comme un pipeline. Leur conception est cependant très complexe, ce qui fait qu'ils ne sont jamais utilisés en pratique. La règle est clairement de séparer les étages dans des circuits séparés, interfacés par des registres.
==La séparation des étages de pipeline==
Découper un processeur en pipeline peut se faire de différentes manières, le nombre et la fonction des étages variant fortement d'un processeur à l'autre. Dans ce qui suit, nous allons voir quelques pipelines simples, faciles à étudier. Nous allons partir d'un processeur sans pipeline, et allons séparer les unités dans plusieurs étages. Le processeur de départ est très simple, nous allons supposer que son chemin de données contient une ALU entière, une unité mémoire et le banc de registres. Nous ajouterons d'autres unités de calcul, comme une FPU ou un multiplieur entier, plus tard.
===Le pipeline Fetch-Exec===
En guise de premier exemple, nous allons prendre le '''pipeline ''Fetch-Execute'''''. IL a seulement deux étages : un qui charge et décode l'instruction, l'autre qui exécute l'instruction. En clair, il sépare le séquenceur et le chemin de données dans deux étages séparés. Il s'implémente en séparant les deux étages par un registre, qui mémorise les signaux de commande en sortie du décodeur d'instruction. Les deux étages sont respectivement nommés ''Fetch'' et ''Exec'', ce qui donne son nom au pipeline. Il s'agit du pipeline le plus simple qui soit, mais il a pourtant été utilisé sur les processeurs Atmel AVR, ainsi que les processeurs PIC.
[[File:The Fetch-Execute Cycle.svg|centre|vignette|upright=2|The Fetch-Execute Cycle]]
Un problème avec ce pipeline est que les deux étages peuvent parfois faire un accès mémoire en même temps. Le pipeline doit lire une instruction à chaque cycle d'horloge, quelle que soit l'instruction. Si l'étape d'''Exec'' exécute un accès mémoire pour lire/écrire une donnée, le processeur fait deux accès concurrents : un pour charger l'instruction, un autre pour lire/écrire la donnée. Il s'agit techniquement d'une situation de dépendance structurelle, mais passons.
La solution simple la plus simple est de passer à une architecture Harvard, avec deux mémoires séparées. Pour un processeur avec un cache, cela revient à utiliser un cache d'instruction séparé du cache de données, ce qui garantit que les deux étages ne se marcheront plus dessus et accéderont chacun à leur cache dédié. Une solution alternative utiliser un cache unique multiport, mais l'essentiel est qu'on puisse lire une instruction et accéder à une donnée en même temps. Précisons que tous les pipelines ont ce problème, sans exception ! Tous les pipelines que nous verrons dans ce cours font face au même problème et la même solution est appliquée.
Le pipeline précédent est de loin le plus simple. Tous les pipelines qui existent ne sont qu'une amélioration de celui-ci. Par amélioration, on veut dire que l'étage de ''Fetch'' est découpé en plusieurs sous-étages, idem avec l'étage d'''Exec''. Cependant, découper le séquenceur ne se fait pas de la même manière que le découpage du chemin de données. Nous allons voir les deux séparément, en commençant par le découpage du séquenceur.
===Le pipelining du séquenceur===
Le '''pipeline ''Fetch-Decode-Exec''''' sépare l'unité de chargement de l'unité de décodage d'instruction dans deux étages séparés. Le résultat est un pipeline à trois étages : le premier étage correspond au calcul du ''program counter'' et à l'accès au cache, le second au décodage de l’instruction, le troisième à l’exécution de l'instruction par le chemin de données. Ces trois étapes sont souvent désignées sous les noms de ''Fetch'', ''Decode'' et ''Exec'', termes que nous utiliserons dans la suite de ce cours. Encore une fois, ils donnent leur nom au pipeline. Le pipeline est donc le suivant :
* ''Fetch'' : chargement de l'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Bien que très simple, le pipeline ''Fetch-Decode-Exec'' a été utilisé dans des processeurs commerciaux grand public. Par exemple, les tout premiers processeurs ARM jusqu'aux processeurs ARM7 utilisaient un pipeline ''Fetch-Decode-Exec''.
L’implémentation sépare l'unité de chargement et le décodeur d'instruction avec un registre. Pour cela, on profite du registre d'instruction, qui était déjà entre l'étage de chargement et l'étage de décodage. L'implémentation est donc terriblement simple.
[[File:Pipeline à trois étages.png|centre|vignette|upright=2|Pipeline à trois étages]]
Il est possible de scinder l'étape de chargement en deux étapes séparées : la mise à jour du ''program counter'' et l'accès au cache d'instruction. La première étape ne fait pas que calculer le ''program counter'', mais gère aussi les branchements au sens large. Elle incorpore des techniques de prédictions de branchement que nous verrons dans le chapitre suivant. En tout cas, elle fait suffisamment de travail pour avoir un étage dédié, le travail prenant facilement un cycle d'horloge complet. Le pipeline devient alors le suivant :
* ''PC Update'' : mise à jour du ''program counter'' ;
* ''Fetch'' : accès au cache d'instruction ;
* ''Decode'' : décodage de l'instruction ;
* ''Exec'' : exécute l'instruction.
Précisons cependant une chose importante : la gestion des branchements se fait dans plusieurs étages. Le calcul des conditions est réalisé dans l'unité de calcul, donc dans l'étage d'''Exec''. Mais une fois l'adresse de destination connue, celle-ci doit être écrite dans le ''program counter'' , qui est lui dans l'étage de ''PC Update''.
[[File:Pipeline PC-Fetch-Decode-Exec.png|centre|vignette|upright=2.5|Pipeline PC-Fetch-Decode-Exec]]
Les processeurs modernes implémentent de nombreuses optimisations, qui partent du principe que le calcul du ''program counter'', l'accès au cache d'instruction, et le décodage sont trois étapes séparées. Mais nous verrons cela en temps voulu, dans le chapitre nommé "Les optimisations du chargement des instructions". Pour le moment, retenez que le début du pipeline peut être scindé en trois étapes nommées PC, ''Fetch'' et ''Decode'', qui peuvent elles-mêmes être séparées en sous-étapes pipelinées.
===Le pipelining du chemin de données===
Les pipelines précédents sont très simples à comprendre, car ils collent avec la séparation entre ''Program counter'', unité de chargement, décodage et chemin de données, qui nous est familière. Les pipelines qui vont suivre pipelinent leur chemin de données, d'une manière qui est moins intuitive. Autant découper le séquenceur en plusieurs circuits séparés est simple, autant découper le chemin de données ne l'est pas. Le chemin de données contient au minimum une unité de calcul, un banc de registres et l'unité mémoire (et un paquet d'interconnexion qu'on met sous le tapis). Reste à voir comment les pipeliner.
Pour simplifier, nous allons prendre le cas d'un processeur RISC, pour éliminer les instructions ''load-op'', qui sont particulièrement problématiques avec un pipeline. Vu que le processeur a une architecture LOAD/STORE, les instructions d'accès mémoire vont dans l'unité mémoire, les instructions de calcul vont dans l'ALU entière. Il parait donc naturel de mettre les deux dans un même étage de pipeline, qu'on appellera l''''étage d’exécution'''. L'étage d'exécution utilise donc soit l'une, soit l'autre, pas les deux.
L'étage d'exécution communique avec le banc de registre : pour récupérer les opérandes lues, pour enregistrer le résultat d'un calcul, ou la donnée lue par une lecture. Intuitivement, on se dit qu'il faudrait un étage pour lire dans le banc de registre et un autre pour écrire dedans. Et cela implique que le banc de registre est multiport, avec des ports de lecture et d'écriture séparés.
Le résultat est une organisation en trois étages :
* Chargement d’opérandes : lecture des opérandes depuis les registres.
* Exécution : exécution de l'instruction dans l'unité de calcul ou l'unité mémoire.
* Enregistrement : écriture du résultat dans les registres.
[[File:Pipeline à 7 étages, avec mode d'adressae immédiat et absolu gérés.png|centre|vignette|upright=2.0|Pipeline à 7 étages naïf.]]
Le pipeline précédent met l'unité mémoire et l'ALU entière en parallèle dans l'étage d'exécution. Soit l'une est utilisée, soit c'est l'autre. En clair, on met plusieurs unités en parallèle dans le même étage d'exécution. Et il est possible de faire pareil avec d'autres unités. Par exemple, on peut mettre le ''barrel shifter'' en parallèle de l'ALU entière et de l'unité mémoire. Les décalages iront dans le ''barrel shifter'', les autres opérations entières dans l'ALU entière, et les instructions LOAD/STORE dans l'unité mémoire.
Une autre possibilité, complémentaire des précédentes, est d'utiliser une unité de calcul dédiée aux branchements, qui est placée dans l'étage d'exécution. L''''unité de branchements''' est la seule à être reliée au séquenceur, l'unité de calcul normale ne l'est pas. Cela simplifie grandement la conception du processeur, sans impact sur les performances.
[[File:Pipeline de longueur fixe avec unités de calcul en parallèle.png|centre|vignette|upright=2|Pipeline de longueur fixe avec unités de calcul en parallèle]]
Une subtilité avec un pipeline de ce genre est que les instructions ont des besoins différents, mais qu'on les force à rentrer dans un pipeline qui a exactement N étages. La conséquence inévitable est que certains étages de pipeline leurs sont inutiles. Et le mieux, pour s'en rendre compte, et de regarder quelques exemples.
Les instructions MOV de copie entre registre n'utilisent ni l'unité de calcul ni l'unité mémoire. Elles lisent un registre et le recopient directement dans le registre destination? Seul le banc de registre est utilisé, l'étage d’exécution ne sert pour ainsi dire à rien. Une solution pour implémenter ces instructions est que l'ALU entière incorpore l'opération ''pass through'' qui recopie une entrée sur sa sortie. Les instructions MOV de copie inter-registres passent donc par l'ALU entière, mais celle-ci effectue donc un "NOP", elle ne fait rien. La conséquence est que l'étage d'exécution ne fait rien, mais l'instruction MOV quand même à travers celui-ci.
[[File:Inactivation d'un étage de pipeline avec des multiplexeurs.png|centre|vignette|upright=2|Inactivation d'un étage de pipeline avec des multiplexeurs]]
Le dernier étage enregistre quelque chose dans les registres : soit la donnée lue en mémoire, soit le résultat fournit par l'ALU. Il regroupe le port d'écriture du banc de registre, et quelques MUX. Et l'instruction STORE ne l'utilise pas, vu qu'elle n'a rien à enregistrer dans les registres. Pareil avec les branchements : ils n'enregistrent rien dans les registres, ils altèrent uniquement le ''program counter''. Cependant, les branchements et STORE passent quand même par cet étage, elles n'ont pas le choix. Simplement, elles ne font rien lors de cet étage.
Les branchements inconditionnels directs sont eux encore plus problématiques : ils sont exécutés par le décodeur d'instruction. Qui dit branchement inconditionnel dit que l'on n'a pas besoin de tester de conditions, donc pas besoin d'utiliser l'ALU entière, ni l'unité de branchement. De plus, qui dit direct dit que l'adresse de destination est encodée dans le branchement, le décodeur peut l'extraire sans lire les registres. Et évidemment, pas besoin d'enregistrer quoique ce soit dans les registres. Les étages du chemin de données sont totalement inutilisés.
Pour exécuter un tel branchement, l'unité de décodage envoie un NOP dans le chemin de données. En clair : elle dit aux étages de ne rien faire pendant un cycle d'horloge. Un tel NOP n'est cependant pas une instruction, mais une micro-opération NOP, interne au pipeline, qui a été générée lors du décodage. Nous verrons que de tels NOP sont utilisés pour bien d'autres choses. Aussi, je leur donner le nom de '''NOP de pipeline''', terme de mon invention.
Ceci étant dit, passons à tout autre chose. Le pipeline précédent met en parallèle ALU entière et unité mémoire. Et cela pose un problème pour gérer certains modes d'adressages, qui demandent de faire des calculs d'adresse. Les calculs d'adresse ne peuvent pas être fait dans l'ALU entière, vu qu'elle est en parallèle de l'unité mémoire, pas avant elle. La seule solution est d'utiliser une unité de calcul d'adresse dédiée, placée avant l'unité mémoire. L'unité de calcul d'adresse est très simple, plus économe en circuit et plus rapide qu'une ALU entière vraie. Il y a donc une petite duplication de circuit, mais elle est nécessaire.
Une solution alternative fait faire les calcul d'adresse dans l'ALU entière. Le chemin de données est alors découpé en quatre étapes :
* ''Read Operands'' (RO) : lecture des opérandes dans les registres ;
* ''Execution'' (EXEC) : exécution d'un calcul dans l'unité de calcul ;
* ''Memory'' (MEM) : accès mémoire dans l'unité mémoire, facultatif ;
* ''Writeback'' (WB) : enregistrement d'une donnée dans les registres.
[[File:Pipeline à 7 étages naïf.png|centre|vignette|upright=2|Pipeline à 7 étages naïf]]
Un problème est que l'étage ''Memory'' (MEM) n'est pas utilisé pour les instructions de calcul, les branchements et toutes les instructions à l'exception de LOAD et STORE. Le problème précédent, à savoir des étages qui ne font rien, est là bien plus visible, plus fréquent et donc plus génant.
Et au-delà de ça, même pour les instructions LOAD et STORE, il y a des étages inutilisés. Par exemple, avec le mode d'adressage indirect à registre, il n'y a pas de calcul d'adresse, l'étage d'exécution est inutilisé. Et c'est encore pire avec le mode d'adressage absolu, vu que l'adresse est fournie par le séquenceur. Le banc de registre n'est pas utilisé non plus, l'étage ''Read Operands'' est donc inutile !
Et pourtant, malgré ses défauts, nous verrons qu'une variante de ce pipeline a été utilisée sur des processeurs commerciaux.
===La propagation des signaux de commande dans le pipeline===
Découper le chemin de données demande de placer des registres de pipeline au bon endroit. Les registres propagent les données adéquates, mais aussi les signaux de commande générés par le décodeur, qui configurent le chemin de données. En effet, relier l'unité de décodage aux circuits incriminés directement ne marcherait pas. Les signaux de commande arriveraient immédiatement aux circuits, alors que l'instruction n'a pas encore atteint ces étages ! Les signaux de commande doivent traverser le pipeline et être consommés par les étages adéquats, et doivent donc être propagé d'étage en étage, via les registres de pipeline. Cette méthode sera utilisée sur tous les pipelines où le chemin de données contient plusieurs étages.
[[File:Propagation des signaux de commande dans un pipeline à 7 étages.png|centre|vignette|upright=2.0|Propagation des signaux de commande dans un pipeline à 7 étages.]]
Il faut noter que quand on parle de signaux de commande, on parle dans un sens très large. Et cela inclus les constantes immédiates du mode d'adressage immédiat. les constantes sont fournies par le séquenceur et sont propagées d'étage en étage jusqu'à atteindre l'étage avec l'unité de calcul. Il en est de même pour le mode d'adressage absolu, qui fournit une adresse mémoire dans l'instruction. L'adresse est extraite par le décodeur d'instruction, propagée dans le pipeline jusqu'à atteindre l'unité mémoire.
[[File:Modes d'adressages absoluss et immédiats avec un pipeline.png|centre|vignette|upright=2.0|Modes d'adressages absoluss et immédiats avec un pipeline]]
==Le pipeline RISC classique et ses variantes==
Les pipelines précédents sont conçus pour être relativement simples et pédagogiques. Mais la plupart des autres cours, notamment les ''textbooks'' américains, préfèrent utiliser un pipeline appelé le '''pipeline RISC classique'''. Il a été utilisé sur les premiers processeurs RISC : les CPU MIPS, SPARC, le 88000 et quelques autres, des processeurs très influents et très populaire à leur époque. Le pipeline RISC est présent dans presque tous les cours qui abordent le sujet du pipeline, que ce soit pour son intérêt historique, pour son apparente simplicité, ou par habitude pédagogique.
Le pipeline RISC classique est cependant loin d'être idéal au niveau pédagogique. Sa simplicité apparente est trompeuse et il a de nombreuses particularités qui ne sont pas évidentes à comprendre. Il permet d'aborder précocement les concepts de dépendances de données/structurelles, mais nous n'utiliserons pas cette approche dans ce cours/wikilivres. Je vais cependant parler du pipeline RISC classique rapidement, ce qui vous sera utile si jamais vous souhaitez consulter d'autres sources, des cours, des articles wiki ou autres.
Il s'agit d'un pipeline à 5 étages, qui met l'ALU et l'unité mémoire dans deux étages consécutifs. Quelques cours utilisent parfois une version simplifiée à 4 étages, qui fusionne l'étage d'exécution et d'accès mémoire. Les 5 étages sont les suivants :
* ''Instruction Fetch'' (IF) pour l'étage de ''Fetch'' ;
* ''Instruction Decode'' (ID) pour l'étage de décodage ;
* ''Execution'' (EXEC) pour l'étage d'execution ;
* ''Memory'' (MEM) pour l'étage d'accès mémoire ;
* ''Writeback'' (WB) pour l'étage d'enregistrement dans les registres ;
Il est conçu pour une architecture RISC, qui gère quelques modes d'adressage mineurs. L'unité de décodage est très simple, elle n'a pas de microcode. C'est nécessaire pour que le décodage se fasse en un seul cycle d'horloge. De plus, les processeurs RISC utilisent des encodages simples, faciles à décoder. Leurs instructions sont de taille fixes, elles étaient toutes codées sur 32 bits sur les processeurs avec ce pipeline.
===La lecture des opérandes se fait en parallèle du décodage===
Une subtilité est que la lecture des opérandes se fait en parallèle du décodage, et non après. C'est étrange, pas intuitif, et cela complexifie le pipeline. Les raisons derrière ce choix sont multiples. La première est que cela permet d'exécuter les branchements conditionnels en avance. La seconde est une histoire de dépendances structurelles que nous ne pouvons pas aborder ici.
Avec ce choix, l'équilibrage du pipeline n'est pas parfait, vu que l'étage de décodage fait beaucoup de choses. Il faut dire qu'on a fusionné l'étage de décodage avec l'étage ''Read Operand''. Rien qui ne puisse être compensé par l'ajout des multiplications dans l'ALU, ceci dit. Mais le vrai problème est que le décodage devient beaucoup plus compliqué, car il faut extraire des numéros/noms de registre précocement lors du décodage.
Heureusement, le pipeline RISC classique était prévu initialement pour des processeurs MIPS, des processeurs RISC avec un encodage très simple. Le décodage des instructions était très simple, les opcodes étaient tous à la même place, les noms/numéros de registres aussi, on pouvait extraire l'opcode et le noms/numéros de registre au tout début du décodage. Les noms/numéros de registre étaient extraits simplement avec quelques multiplexeurs, puis envoyés au banc de registre pendant que l'unité de décodage finissait son travail. Mais notons que cela ne fonctionne que sur des processeurs RISC, et encore : il faut que l'encodage des instructions soit assez simple pour le permettre. C'est un détail qui ne se généralise pas.
===Les branchements, leur exécution et leur décodage===
L'étage de chargement dispose d'une première optimisation assez intéressante : il effectue la mise à jour du program counter en parallèle de l'accès mémoire. Le ''program counter'' est directement relié à l'entrée d'adresse du cache ou de la mémoire, son incrémenteur et les MUX pour les branchements sont situés après. Par contre, la gestion des branchements est assez complexe.
Les architectures MIPS peuvent faire un branchement qui s'exécute si deux registres sont égaux. Le calcul d'un branchement demande donc de comparer deux registres, puis de décider s'il faut brancher selon le résultat. Une première possibilité, la plus intuitive, place le comparateur dans l'étage d'exécution. D'autres implémentations utilisent l'unité de calcul entière pour faire la comparaison, ce qui est équivalent.
[[File:MIPS Architecture (Pipelined).svg|centre|vignette|upright=2.5|MIPS Architecture (Pipelinée)]]
Une solution alternative exécute la comparaison dans l'unité de décodage, juste après la lecture des registres ! L'avantage de faire ainsi n'est pas évident, mais le deviendra dans le chapitre suivant. Mais cela se fait au prix d'un étage de décodage bien chargé, le pipeline est très déséquilibré. LLe schéma précédent montre un exemple, mais sur une variante 4 étages du pipeline RISC classique.
[[File:Conditional Microprocessor 2.0 Pipeline Design 1.1.png|centre|vignette|upright=2.0|Le circuit noté JUMP est le comparateur qui sert à comparer deux registres. CF est un bit du registre d'état utilisé pour les branchements.]]
Un autre détail est que le calcul des branchements relatifs est réalisé dans l'unité de calcul. Quelques processeurs exécutaient les branchements conditionnels dans l'ALU entière. Pour rappel, les branchements relatifs ajoutent une constante au contenu du ''program counter'', afin de brancher X adresses après/avant l’instruction courante. Il y a donc une addition à faire, qui est réalisée par un additionneur dédié dans l'unité de chargement, du moins dans la majorité des cas. Mais le pipeline RISC préfère utiliser l'additionneur de l'ALU entière pour cela, pour diverses raisons. Le ''program counter'' est donc propagé dans le pipeline, et est connecté à un multiplexeur en entrée de l'ALU entière.
==L'optimisation du pipeline de données==
Le pipeline vu juste avant est conçu pour être logiques, avec une séparation entre chargement/décodage/exécution/accès au registre qui est relativement intuitive. Le pipeline précédent est un bon point de départ, qu'on peut améliorer. Cependant, cette implémentation réelle n'aurait pas des performances optimales en situation réelle. La raison est que les étages du pipeline ne sont pas équilibrés. Voyons ce que ça veut dire.
Les étages de pipeline ne font pas la même chose. Prenons par exemple un pipeline basique, de type ''Fetch-Decode-Exec''. Les trois étages ne mettent pas le même temps pour faire leur travail. Pour donner un exemple pas fait pour être réaliste, charger une instruction depuis le cache prend 6 µs, la décoder prend 12 µs pour le décodeur, et l'exécuter prend 8 µs. Sans optimisations, la durée d'un cycle d'horloge devra se caler sur l'étage le plus lent. Nous l'appellerons l''''étage limitant''', terme qui est assez parlent.
===L'accès aux caches L1 est lent===
Les étages limitants sont souvent ceux qui demandent de faire des accès mémoire. Ils sont exactement deux : l'étage MEM qui accède au cache de données, mais aussi l'étage ''Fetch'' qui charge une instruction depuis le cache d'instruction. Les deux caches L1 ont un temps d'accès similaire, ce qui fait qu'ils sont tous deux des étages limitants.
Supposons qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les autres étages se calaient dessus et s'adaptaient. Par exemple, si un accès au cache fait 50 µs, alors les autres étages essayaient de se rapprocher de 50 µs. Si un calcul dans l'ALU fait 10 µs, et lire/écrire les registres en fait 15, alors on peut utiliser un étage ''Exec'' unique, pas besoin de scinder lecture des opérandes et exécution. Et l'exemple se généralise assez bien : il est intéressant de fusionner deux étages consécutifs pour obtenir un étage aussi rapide que l'étage limitant.
L'unité de calcul peut en profiter pour faire des calculs complexes, comme les multiplication ou divisions. Précisons qu'il y a des pipelines qui gèrent les instructions multi-cycles, tous les processeurs modernes sont dans ce cas-là. Mais ils seront vus dans des chapitres à part. Pour le moment, nous devons nous concentrer sur les pipelines 1-cycle, où toutes les opérations se font en un seul cycle d'horloge.
[[File:Pipeline à 6 étages simplifié.png|centre|vignette|upright=2|Pipeline à 5 étages naïf]]
La solution précédente fait en sorte de se '''caler sur l'étage limitant''', quitte à réorganiser le pipeline. La répartition du travail entre étages est donc modifiée. L'inconvénient est qu'elle réduit le nombre d'étages, et donc le nombre d'instructions en vol. Mais une seconde technique permet au contraire de pipeliner encore plus le pipeline. L'idée est que les étages limitants sont eux-même pipelinés, scindés en circuits plus simples séparés par des registres.
Dans l'exemple précédent, l'accès au cache d'instruction ne prend qu'un seul cycle d'horloge. Mais sur les processeurs modernes, l'accès au cache d'instruction prend deux à trois cycles. Et ce n'est pas incompatible avec la présence d'un pipeline, à condition que le cache lui aussi soit pipeliné. En clair, la solution est de '''pipeliner les caches L1'''. Lire une instruction se fait alors en plusieurs étapes. Nous verrons comment c'est possible dans un chapitre dédié nommé "Le parallélisme mémoire au niveau du cache".
Les premiers processeurs RISC faisaient en sorte qu'un accès aux caches L1 se fasse en un cycle d'horloge. Les premiers processeurs x86 étaient de ce type, les derniers étant la micro-architectures K5 d'AMD et le premier Pentium. La microarchitecture K6 d'AMD a pipeliné son cache, idem pour la microarchitectire P6 du Pentium 2 et 3. Utiliser un cache 1-cycle est donc une solution parfaitement viable, qui n'est abandonnée que quand le besoin de performance se fait sentir.
===Les décodeurs sont lents sur les CPU CISC===
L'accès au cache est l'étage limitant sur les processeurs RISC. Pour les processeurs CISC, les choses sont parfois différentes. L'étage de décodage est lui aussi assez complexe, vu qu'il doit gérer des instructions de taille variable. Un décodeur CISC devient rapidement l'étage limitant, surtout si les caches L1 sont pipelinés. A problème similaire, solution similaire. Il est possible de '''pipeliner le décodeur d'instruction''', à savoir le scinder en plusieurs circuits consécutifs, séparés par des registres de pipeline. La scission peut se faire à des "moments de décodage" différents,.
Une scission assez logique sépare la gestion des instructions de taille variable du décodage lui-même. La gestion des instructions de taille variable implique d'utiliser des ''barrel shifter'' et des opérations de masquage, qui sont couteux en circuits et assez "lents". Le décodage proprement dit vient après. Et les deux peuvent être séparées. Un premier étage de décodage transforme les instructions de taille variable en "instructions de taille fixe", le second étage décode ces dernières. les deux étages de décodage sont souvent appelés les '''étages de pré-décodage''' et l''''étage de décodage''' proprement dit.
: Le terme pré-décodage ne doit pas être confondu avec le pré-décodage dans le cache d’instruction, qu'on verra dans quelques chapitres.
Mais la scission peut être encore plus profonde. Le décodage lui-même peut être découpés en plusieurs étapes, qui sont elles-mêmes pipelinées. Mais nous ne pouvons pas vraiment en dire plus, je ne peux pas donner de généralités là-dessus, les détails dépendent vraiment du jeu d'instruction et de l'implémentation du décodeur d'instructions.
Les processeurs modernes ont un pipeline de 8-20 étages, dont au moins un étage pour le calcul du ''program counter'' et 2 à 4 cycles pour l'accès au cache d'instruction, et tout autant de cycles pour le décodage. Et il est même possible de pipeliner l'étape de calcul du ''program counter''. Pour donner un exemple, le processeur Pentium Pro avait un pipeline de 14 étages, dont 7 étaient liés au décodage/chargement : deux étages pour le calcul du ''program counter'', trois pour l'accès au cache d'instruction, deux pour le décodage des instructions.
==La performance d'un pipeline==
Revenons un peu sur les pipelines synchrones. L'usage d'un pipeline augmente les performances, mais essayons de comprendre pourquoi. La raison est que l'on peut exécuter plusieurs instructions en même temps. Mais il se pourrait que cela aie d'autres effets, par exemple sur le temps d’exécution des instructions ou la fréquence. Pour comprendre toutes les conséquences de l'usage d'un pipeline, le mieux est d'étudier l'impact du pipeline sur divers paramètres du processeur : fréquence, temps d’exécution, parallélisme d'instruction, et autres. Nous allons nous focaliser sur la fréquence, le temps d’exécution d'une instruction et le nombre d'instructions exécutées en parallèle.
===La performance théorique d'un pipeline idéal (approche simplifiée)===
Pour commencer, nous allons voir cas d'un pipeline idéal, c'est à dire que nous allons négliger le fait qu'il y a des registres entre les étages du pipeline. Ils ont un temps de propagation non-nul, et ont donc un effet sur la fréquence et sur la latence des instructions. Mais pour simplifier les calculs, nous allons négliger le temps de propagation des registres inter-étages.
Sur les processeurs réels, certains étages possèdent un chemin critique plus long que d'autres. On est alors obligé de se caler sur l'étage le plus lent, ce qui réduit quelque peu le gain. La durée d'un cycle d'horloge doit être supérieure au temps de propagation de l'étage le plus lent.
[[File:Pipelining réel d'un circuit.png|centre|vignette|upright=2.5|Pipelining hétérogène d'un circuit]]
Cependant, nous allons supposer que les étages sont assez bien équilibrés, de manière à avoir le même temps de propagation. Le pipeline qu'on va étudier est donc un cas irréaliste de pipeline, idéal. Prenons un processeur qui exécute toutes ses instructions en un seul cycle sur un processeur et découpons ce processeur en 5 étages équilibrés. Voici ce que l'on obtient :
[[File:Effet de l'usage d'un pipeline sur la fréquence d'un processeur.png|centre|vignette|upright=2.0|Effet de l'usage d'un pipeline sur la fréquence d'un processeur.]]
Le schéma précédent montre que la fréquence change entre les deux situations car le cycle d'horloge n'est pas déterminé par l'instruction entière, mais par le temps de propagation d'un étage. Et cette durée correspond à la durée d'un cycle, divisée par le nombre d'étages. On passe donc c'un processeur dont la fréquence est de :
: <math>f = {1 \over t}</math>, avec t le temps d’exécution d'une instruction (sans pipeline).
à un processeur de fréquence :
: <math>f_\text{pipeline} = \frac{1}{t \over N} = N \times f</math>, avec N le nombre d'étages.
On voit que la fréquence a été multipliée par le nombre d'étages avec un pipeline ! En clair, l'usage d'un pipeline permet donc de multiplier la fréquence par un coefficient plus ou moins proportionnel aux nombres d'étages.
Par contre, le temps d’exécution d'une instruction ne change pas avec ou sans pipeline ! Le pipeline permet d’exécuter plusieurs instructions en même temps, mais chaque instruction met presque autant de temps à s’exécuter avec un pipeline idéal. Elle est juste découpées en plusieurs étapes courtes au lieu d'être faite en une fois. Une autre manière de le voir est de remarquer que si la fréquence est multipliée par N, c'est compensée par le fait que l'instruction prend maintenant N étages pour s’exécuter, et donc N cycles d'horloge.
Étudier l'impact sur la performance du processeur n'est pas trivial. Peut-être avez-vous pensé à utiliser l'équation vue dans le chapitre sur la performance d'un ordinateur :
: <math>\text{Temps d'execution d'un programme} = N_{instructions} \times \frac{CPI}{f}</math>, avec f la fréquence, CPI le nombre de cycles moyen par instruction, et <math>N_{instructions}</math> le nombre d'instructions à exécuter.
Mais l'équation en question ne fonctionne que si on suppose que les instructions sont exécutées l'une après l'autre. Un bon moyen de s'en rendre compte est de remarquer qu'avec un pipeline, f et CPI sont tous les deux multipliés par N, ce qui fait que le terme de droite ne change pas, de même que le terme N reste le même. Mais alors d'où vient l'amélioration de performance tant promise ?
Elle vient du fait que le pipeline exécute plusieurs instructions simultanées, au lieu d'une seule sans pipeline. il y a précisément une instruction dans chaque étage maximum, ce qui multiple la puissance de calcul maximale par le nombre d'étages du pipeline. En clair, le pipeline ne réduit pas le temps d’exécution d'une instruction, mais augmente les performances en en exécutant plusieurs presque simultanément. Si la latence reste la même, le débit du processeur augmente.
===La performance théorique d'un pipeline avec des registres inter-étages===
En théorie, le raisonnement précédent nous dit que le temps d’exécution d'une instruction est le même sans ou avec un pipeline. Cependant, il faut prendre en compte les registres intercalés entre étages du pipeline, qui ajoutent un petit peu de latence. Nous allons noter <math>\tau</math> le temps de propagation d'un de ces registres et refaire les calculs précédents, en tenant compte de ce terme. Le temps de parcours d'un étage devient :
: <math>t_\text{étage} = \frac{t}{N} + \tau</math>
En multipliant par N, on obtient le le temps d’exécution d'une instruction. On voit que ce dernier est égal au temps sans pipeline, auquel on ajoute la latence des registres inter-étages. Le temps d’exécution d'une instruction est donc allongé avec un pipeline.
: <math>T = N \times \left( \frac{t}{N} + \tau \right) = t + \tau \times N</math>
La fréquence du processeur est l'inverse de <math>t_\text{étage}</math>, ce qui donne :
: <math>f = \frac{1}{\frac{t}{N} + \tau} = \frac{N}{t + \tau \times N}</math>
L'équation précédente nous dit que la fréquence du processeur avec un pipeline augmente, comparé à la fréquence du même processeur mais sans pipeline, mais pas autant que dans le cas idéal précédent. Elle n'est pas multipliée par le nombre d'étage, mais un peu en-dessous. Passer de 1 étage à 2 donne un gain en fréquence, mais passer de 20 à 40 aura un effet beaucoup plus faible. Les rendements sont rapidement décroissants, la durée d'un étage étant de plus en plus dominée par la latence des registres.
On peut calculer le gain en comparant la fréquence avec et sans pipeline :
: <math>\frac{f_\text{Avec pipeline}}{f_\text{Sans pipeline}} = \frac{N t}{N \tau + t} = \frac{N}{{N \tau \over t} + 1}</math>
Le résultat est inférieur à N et on voit l'influence des N registres. Le terme <math>{N \tau \over t}</math> correspond au pourcentage de temps passé à traverser les registres par rapport au temps de propagation sans les registres. Par exemple, si le temps de propagation des registres prend la moitié du temps de propagation, alors ce terme vaut 1 et la fréquence est divisée par deux.
===Le monde réel : l'exemple des processeurs Intel===
Les développements précédents sont très intéressants, mais il reste à voir ce que cela donne dans le monde réel. Pour cela, il faut prendre un échantillon de processeurs et comparer leur fréquence en fonction de la longueur de leur pipeline. Mais il faut tenir compte de la finesse de gravure, qui impacte fortement la fréquence du processeur et fausse les comparaisons. Il est possible de déterminer une une ''fréquence relative'', à savoir la fréquence à finesse de gravure égale, par rapport à un processeur de référence.
Et on s’aperçoit que cette fréquence relative est fortement reliée aux nombres d'étages du pipeline. Un processeur à haute fréquence relative a un pipeline plus long que ses concurrents de fréquence relative plus basse. La relation n'est pas proportionnelle, du fait des registres inter-étages et de l’hétérogénéité des latences intra-étage. Par exemple, regardons la fréquence relative des processeurs Intel d'avant le Pentium 4 (les seuls pour lesquels j'ai les données). Voici ce qu'on obtient :
[[File:Fréquences relatives processeurs Intel pré-Pentium 4.png|centre|vignette|upright=2|Fréquences relatives processeurs Intel pré-Pentium 4]]
Et comparons la longueur de leur pipeline :
{|class="wikitable"
|-
! Processeur
! Longueur du pipeline
|-
| 286
| rowspan="4" | 5
|-
| 386
|-
| 486
|-
| Pentium
|-
| Pentium 2/3
| 14
|-
| Pentium 4
| 31 à 20 selon les modèles
|}
On s’aperçoit qu'il n'y a pas proportionnalité exacte, mais que la tendance est bonne. La profondeur du pipeline a presque triplé au passage au Pentium 2/3, mais cela s'est traduit par une hausse de fréquence de seulement 50%. Pour le Pentium 4, sa fréquence a sextuplé par rapport aux premiers CPU Intel, mais la fréquence a été multipliée par seulement 2,5.
Avant les années 2000, la fréquence était un argument marketing puissant. Ce qui a poussé certains fabricants de processeurs à créer des processeurs avec un nombre d'étages élevé pour les faire fonctionner à très haute fréquence. C'est ce qu'a fait Intel avec le Pentium 4, dont le pipeline faisait 20 étages pour les Pentium 4 basés sur les architectures Willamette et Northwood, 31 étages pour ceux basés sur les architectures Prescott et Cedar Mill. L'approche a rapidement montré ces limites, poussant Intel à revenir à un nombre d'étages raisonnables pour les processeurs Core 2 Duo.
==Le blocage du pipeline==
Avec ce qu'on a dit plus haut, il semblerait que l'implémentation d'un pipeline soit assez simple. Mais dans la réalité, un pipelines doit gérer des situations assez peu intuitives, qui grippent sa belle mécanique bien huilée. Un pipeline est un peu l'équivalent d'une chaine de montage, avec une séquence d'étapes à exécuter dans un ordre bien précis. Mais quelques évènements peuvent gripper cette mécanique. Il arrive qu'un étage soit bloqué pendant plusieurs cycles, pour des raisons qu'on expliquera plus bas. Dans ce cas, le processeur doit gérer la situation en bloquant le pipeline. Et au lieu de parler en généralités, abordons le sujet avec un exemple très simple : les défauts de cache.
===La gestion des défauts de cache===
Les pipelines précédents doivent gérer les ''défauts de cache'', et ceux-ci mettent plusieurs cycles pour se terminer. Prenons une instruction mémoire, qui lit une donnée. En temps normal, la lecture se fait dans le cache L1, l'accès mémoire se fait en un cycle. Mais lors d'un défaut de cache, l'instruction doit attendre plusieurs cycles : quelques cycles pour lire dans le cache L2, beaucoup plus pour lire dans le L3, une bonne centaine pour la mémoire RAM. Et le processeur doit gérer la situation.
En cas de défaut de cache, le processeur gèle complétement le pipeline. Les instructions cessent de progresser d'un étage à l'autre. Elles restent dans l'étage courante tant que l'accès mémoire n'est pas terminé. Le ''program counter'' est lui aussi gelé : il n'est plus incrémenté et maintient l'adresse en cours, tant que l'accès mémoire n'est pas terminé. Pour cela, l'unité mémoire émet un signal ''MEMORY ACCESS PENDING'', qui est à 1 pendant qu'un défaut de cache a lieu. Le signal est remis à zéro quand l'unité mémoire a terminé son travail et que la donnée lue est disponible. Ce signal est alors utilisé pour geler les registres du pipeline. Un blocage de pipeline de ce type s'appelle un '''''pipeline stall'''''. Nous parlerons de '''blocage de pipeline'''.
[[File:Implémentation d'un pipeline stall lors d'un accès mémoire.png|centre|vignette|upright=2.5|Implémentation d'un pipeline stall lors d'un accès mémoire]]
Il y a plusieurs méthodes pour cela. La première ajoute une entrée ''Enable'', qui autorise les écritures dans le registre du pipeline. En temps normal, il est à 1, ce qui fait que les registres acceptent ce qui vient de l'étage précédent à chaque cycle. Mais lors d'un défaut de cache, il est mis à zéro. Le registre maintient son contenu, tant que l'entrée ''Enable'' est à 0, il ne tient pas compte de qui arrive de l'étage précédent. Les entrées ''Enable'' sont calculées à partir du signal ''MEMORY ACCESS PENDING'', avec quelques portes logiques. Une méthode alternative utilise le ''clock gating'' pour les étages qui doivent être gelés.
Avec une implémentation basique, tous les étages sont gelés. C'est simple, efficace, mais on peut faire un peu mieux. L'optimisation est de geler les étages situés avant l'étage MEM. Ceux situés après peuvent faire leur travail normalement. Précisons que seuls les registres de pipeline sont gelés, mais pas le banc de registre ni les autres circuits. L'étage de ''writeback'', qui enregistre les résultats dans les registres, n'est donc pas gelé. Les instructions précédant l'accès mémoire peuvent donc terminer leur exécution, pendant que le début du pipeline est gelé.
La même méthode peut être utilisée pour gérer les défauts dans le cache d'instruction. La différence est que seuls les étages avant le cache d'instruction sont gelés. Concrètement, le calcul du ''program counter'' est alors gelé tant que le défaut de cache n'est pas résolu. Vu qu'aucune donnée n'est lue, le décodeur exécute des NOPs de pipeline tant que le défaut de ache est en cours. C'est seulement quand le défaut est terminé et qu'il recoit une nouvelle instruction qu'il cesse d'envoyer des NOPs de pipeline.
===La gestion des instructions multicycles===
Les pipelines précédents ont tous un point commun : toutes les instructions s'exécutent en un cycle d'horloge, pas un de plus, pas un de moins. Les opérations arithmétiques sont exécutées en un seul cycle d'horloge dans l'ALU entière. Et ce y compris les instructions "complexes" comme les multiplications ou divisions. Les accès mémoire aussi se font aussi en un seul cycle d'horloge, sauf en cas de défaut de cache. Il s'agit d'un cas fortement irréaliste, aucun processeur avec un pipeline ne fait dans le monde réel.
Les processeurs commerciaux gèrent des '''instructions multicycles''', qui mettent plusieurs cycles d'horloge pour s'exécuter. Les instructions multicycles sont le plus souvent des multiplications entières, des divisions, ou des opérations flottantes. En conséquence, nous allons ajouter un multiplieur entier et une FPU dans le pipeline, dans l'étage d'exécution. Les multiplications restent pendant plusieurs cycles dans le multiplieur entier, idem pour les opérations flottantes qui restent dans la FPU pendant plusieurs cycles. Les instructions multicycles squattent donc l'étage d'exécution pendant plusieurs cycles.
Le support des instructions multicycles entraine l'apparition de nombreux problèmes, qui sont eux aussi résolus avec des blocages de pipeline. Intuitivement, vu que les instructions multicycles occupent l'étage d'exécution pendant plusieurs cycles, les étages précédents doivent être gelés pendant ce temps. Le pipeline devrait être bloqué pendant que les instructions multicycles utilisent l'étage d'exécution. Mais ce n'est pas exactement la réalité. En théorie, il est possible d'utiliser l'ALU entière pendant que la FPU ou le multiplieur exécutent une instruction multicycle. Cependnat, faire ainsi a des conséquences assez subtiles.
Imaginons qu'une multiplication prenne deux cycles, et l'addition prenne 1 cycle. Prenons une multiplication suivie par une addition. La multiplication commence à s'exécuter au cycle numéro 1, termine son exécution au cycle 2, puis enregistre son résultat au cycle 3. L'addition s’exécute au cycle 2, et enregistre son résultat au cycle 3. Les deux instructions enregistrent leur résultat au même cycle, ce qui n'est pas possible, vu que le banc de registre a un seul port d'écriture. Et s'il s'agit là d'un exemple, il se généralise assez bien.
Un autre problème a lieu avec deux multiplications consécutives. La première occupant le multiplieur pendant X cycles consécutifs, on ne peut pas lancer de nouvelle multiplications pendant ce temps. Avec deux multiplications consécutives, la seconde multiplication doit attendre son tour. Idem avec deux opérations flottantes. Et d'autres problèmes similaires sont possibles, bien qu'assez subtiles.
Les deux problèmes précédents sont des exemples de '''dépendance structurelle''', dont nous avons parlé plus haut. Pour rappel, cela veut dire que deux micro-opérations veulent utiliser un même circuit lors du même cycle d'horloge. Elles n'apparaissent qu'avec des instructions multicycles. Et elles sont gérées en retardant l'exécution des instructions qui suivent une instruction multicycle. Par exemple, si une multiplication prend 5 cycles pour s'exécuter dans le multiplieur, l'instruction suivante est retardée de 4 cycles.
[[File:Pipeline bubble.png|centre|vignette|upright=2|''Pipeline bubble''.]]
La mise en attente des instructions se fait soit dans l'unité de décodage, soit dans un circuit dédié. Dans ce qui suit, nous supposons qu'elle est gérée par une '''unité d'émission''', séparée du décodeur d'instruction. Elle décide des ''pipeline stall'' et gère les NOPs de pipeline.
A chaque cycle, l'unité d'émission reçoit une micro-opération et l'envoie au chemin de données, dans les étages de pipeline suivant. On dit qu'elle '''émet une micro-opération'''. Par contre, dans certaines circonstances, elle retarde l'émission de la micro-opération. Typiquement, quand elle émet une instruction multicycle, elle retarde la micro-opération suivante. Pour cela, les étages précédents sont gelés, que l'unité d'émission envoie des NOPs de pipeline pendant l'exécution de l'instruction multicycle. Une telle situation est un cas particulier de ''pipeline stall'', qui est commandé par le décodeur.
Vous remarquerez que ce n'est pas la FPU ou le multiplieur qui déclenchent le gel du pipeline, contrairement à ce qu'on a avec les défauts de cache. Il y a en effet une différence importante avec les accès mémoire : leur temps de calcul est connu à l'avance. On sait qu'une multiplication prend 5 cycles d'horloge, qu'une addition flottante en prend 8, une multiplication flottante en prend 12, etc. A l'opposé, les défauts de cache ont un temps d'exécution variable, sans compter qu'on ne sait pas à l'avance si un accès mémoire va faire un défaut ou un succès.
===La gestion des dépendances de données===
Nous venons de voir que l'unité d'émission retarde les instructions quand elles ont une dépendance structurelle. Mais ce n'est pas le seul cas où l'unité d'émission doit intervenir. Les cas en question ont tous la même origine : un programme est codé en partant du principe que les instructions s'exécutent de manière séquentielle, l'une après l'autre. Une instruction n'est démarrée que lorsque la précédente est terminée. Mais ce n'est pas le cas sur un pipeline, qui charge une nouvelle instruction à chaque cycle, sans attendre que la précédente soit terminée.
Le pipeline ne pose pas de problèmes si deux instructions consécutives sont indépendantes. Mais en pratique, des dépendances peuvent survenir. Un exemple de dépendance serait le cas où une instruction a comme opérande le résultat d'une instruction précédente. Les deux instructions ont alors une '''dépendance de donnée'''. L'instruction précédente doit avoir enregistré son résultat avant que la seconde le lise dans les registres, sans quoi le résultat de la seconde instruction est faux. Et avec un pipeline, cette contrainte n'est respectée que si l'unité d'émission intervient. L'unité d'émission retarde alors l'instruction suivante, tant que la première n'a pas écrit son résultat dans les registres.
Les '''dépendances de données''' sont nombreuses, mais la plupart n'apparaissent que si le pipeline a des caractéristiques bien précises. Pour les pipelines que nous avons étudié, il n'y en a qu'un seul type : les dépendances où une instruction lit le résultat d'une instruction précédente. Nous verrons les autres types de dépendances en temps voulu. Précision : les dépendances en question sont entre deux instructions en vol, c'est à dire qu'elles sont dans le pipeline en même temps.
Tout cela est bel et bon, mais il y a des dépendances qui ne peuvent pas être réglées en bloquant le pipeline. Il s'agit des dépendances de contrôle, qui sont liées aux branchements, aux interruptions, et aux exceptions matérielles. Et ca sera le sujet de la fin de ce chapitre, ainsi que du suivant.
==Les exceptions précises==
Les processeurs avec un pipeline ont un petit problème avec des exceptions matérielles. Le problème se manifeste pour toutes les interruptions, que ce soit les exceptions matérielles, les interruptions matérielles et les interruptions logicielles. Mais le problème se manifeste surtout pour les exceptions matérielles, pas pour les interruptions matérielles. Les interruptions logicielles sont à part, elles sont techniquement des branchements et nous reparlerons des branchements plus tard dans ce chapitre.
===Les exceptions précises et imprécises===
Les exceptions stoppent l’exécution du programme en cours et effectuent un branchement vers une routine d'interruption. Mais avant que l'exception n'ait été détectée, le processeur a chargé des instructions dans le pipeline en avance. Lorsque l'exception est détectée, elle mémorise le ''program counter'' de la dernière instruction chargée, et reprendre à cet endroit là. La conséquence est que les exceptions sont décalées de quelques cycles par rapport à un processeur sans pipeline.
[[File:Exception et pipeline.png|centre|vignette|upright=2|Exception détectée au 3ème étage d'un pipeline à 4 étage : deux instructions ont été chargées en avance.]]
Et ce décalage de quelques cycles d'horloge est problématique. La majeure partie des exceptions sont traitées comme suit : la routine de l'exception traite la situation, puis le processeur redémarre l'instruction fautive. Un exemple est le cas d'une instruction d'accès mémoire qui déclenche un défaut de page : le défaut de page déclenche une exception, la routine d'exception traite le problème en chargeant la page voulue en RAM, puis re-démarre l'instruction mémoire. Mais re-démarrer l'instruction fautive implique de reprendre l'exécution du programme juste après l'instruction qui a déclenché l'exception. Si des instructions ont été chargées/exécutées en avance, ce n'est pas le cas : on reprend plusieurs instructions après.
En soi, le processeur peut très bien ne rien faire contre ce problème, les exceptions sont alors dites imprécises. Le problème peut être géré de manière purement logicielle. Au pire, cela peut poser quelques problèmes à quelques programmes comme les débuggers, même si les cas sont rares. Par contre, cela implique que l'on ne peut pas traiter les exceptions en re-démarrant l'instruction fautive. Et cela complexifie grandement la gestion des exceptions, le système d'exploitation est plus complexe, le cout logiciel est là. Pour régler ce petit problème, les concepteurs de processeur ont fait en sorte que le processeur gère lui-même exceptions et interruptions correctement. Ils permettent une gestion des exceptions précises.
===Le hardware pour les exceptions précises===
Supporter les exceptions précises demande des modifications assez mineures du processeur. L'idée est que les instructions chargées en avance sont juste annulées, elles n'enregistrent pas leur résultat dans les registres et n’accèdent pas à la mémoire. En effet, si une instruction n'enregistre pas son résultat, que ce soit dans les registres ou la RAM, c'est comme si elle n'avait rien fait. Si une exception a lieu, il suffit de ne pas enregistrer les résultats des instructions suivantes dans les registres, jusqu’à ce que toutes les instructions fautives aient quitté le pipeline.
Les exceptions précises sont gérées dans le dernier étage du pipeline, l'étage de ''writeback'', celui d'écriture dans les registres. Pour cela, tout étage fournit à chaque cycle un '''indicateur d'exception''', un groupe de quelques bits qui indiquent si une exception a eu lieu et laquelle le cas échéant. Ces indicateurs sont propagés dans le pipeline, ils passent à l'étage suivant à chaque cycle. Une fois arrivé à l'étage d’enregistrement, un circuit combinatoire vérifie ces bits pour détecter si une exception a été levée, et interdit l'écriture dans les registres en cas d'exception. Si les écritures sont interdites, elles le sont durant un certain nombre de cycles, dépendant de l'exception levée. On dit qu'on '''vide le pipeline''' des instructions fautives.
[[File:Propagation de l'indicateur d'exception.png|centre|vignette|upright=2|Propagation de l'indicateur d'exception.]]
Il faut aussi qu'une fois l'exception terminée, le programme reprenne là où il faut. Pour cela, il faut restaurer le ''program counter'' pour qu'il pointe vers l’instruction adéquate. Il s'agit de l'instruction qui a déclenché l'exception. Pour cela, même solution : le ''program counter'' est propagé dans le pipeline et est restauré en cas d'exception en prenant le ''program counter'' disponible à l'étage d'enregistrement.
===Les écritures mémoire sont retardées avec des exceptions précises===
Il faut préciser une chose importante : toutes les écritures sont annulées, y compris les écritures en mémoire RAM, y compris celles dans le cache de données. Mais cela pose un problème : les instructions STORE sont censées être exécutées dans l'étage MEM ou Exec, soit avant l'étage de ''Writeback''. Notez bien le censé : ce n'est pas le cas avec les exceptions précises. En réalité, avec des exceptions précises, les instructions STORE font leur écriture dans l'étage de ''Writeback'', l'étage final.
Mais cela implique que l'étage MEM s'occupe des lectures, et l'étage ''Writeback'' des écritures proprement dit. Ils font tous deux un accès au cache, ce qui est une dépendance structurelle. Une solution intuitive, utilise un cache de données multiport. Il a un port de lecture séparé du port d'écriture. Le port de lecture est utilisé pour l'étage MEM, alors que le port d'écriture est utilisé dans l'étage de ''Writeback''.
Mais, il faut aussi propager les écritures d'un étage de pipeline au suivant, jusqu'à atteindre l'étage de ''Writeback''. Pour cela, il est possible d'enchainer plusieurs registres de pipeline. Mais nous avons vu dans le chapitre sur les mémoires FIFO qu'enchainer plusieurs registres à la suite est équivalent à utiliser une mémoire FIFO. Aussi, une solution équivalente une mémoire FIFO qui met en attente les écritures dans le cache. Elle est appelée la ''Write Queue'', '''file d'écriture''' en français.
: La file d'écriture font nous parlons ici ne doit pas être confondue avec le ''Write buffer'' présent dans la hiérarchie mémoire. Les deux sont traduit dans ce cours avec le terme file d'écriture, mais il s'agit de deux choses différentes.
La file d'écriture est une simple mémoire FIFO, contenant quelques entrées. Une entrée contient une donnée et l'adresse d'écriture. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente, faites à tord.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|Store Queue sur un pipeline fixe]]
La file d'écriture peut mettre en attente plusieurs écritures. Le nombre d'écriture à mettre en attente dépend du pipeline. Le nombre exact est égal au nombre d'étages situés entre l'étage MEM et l'étage ''Writeback'', plus 1. Par exemple, les pipelines précédents n'ont pas d'étages entre MEM et ''Writeback'', ce qui fait que la ''Write Queue'' n'a qu'une seule entrée. Il peut être remplacé par un simple registre, mais ce n'est que rarement fait. Mais de nombreux pipelines ont des étages entre les étages MEM et ''Writeback''. Par exemple, s'il y a 4 étages entre les deux, il faut que la file d'écriture puisse mettre en attente 5 écritures. Un autre point est que l'étage MEM peut lui-même prendre plusieurs cycles d'horloge, ce qui est possible si le cache est pipeliné. Dans ce cas, ce nombre de cycle s'ajoute au nombre d'entrées.
{|class="wikitable"
|-
! colspan="3" | Étages MEM !! colspan="4" | Étages autres !! Étage de ''Writeback''
|-
| MEM 1 || MEM 2 || MEM 3 || X || X || X || X || ''Writeback''
|-
! colspan="8" | File d'écriture
|-
| Entrée 1 || Entrée 2 || Entrée 3 || Entrée 4 || Entrée 5 || Entrée 6 || Entrée 7 || Entrée 8
|}
Pour résumer,
: <math>\text{Capacité de la file d'écriture} = \text{Nombre d'étages MEM} + \text{Nombre d'étages entre MEM et WB} + 1</math>
===Les utilisations détournées du système d'exception précise===
Une utilisation "détournée" du système d'exceptions précise est la gestion des défauts de cache. L'idée est qu'un défaut de cache lève une exception matérielle. Les instructions qui suivent la lecture/écriture fautive sont alors propagées dans le pipeline et sont annulées à la toute fin. Une fois le pipeline vidé, l'instruction fautive est re-exécutée une fois que le défaut de cache est terminé. En clair, on vide le pipeline avant de le re-démarrer. Un simple compteur est utilisé pour temporiser le re-démarrage du pipeline.
Les exceptions précises sont aussi utilisées pour corriger des problèmes liés aux branchements, ce qui nous amène au sujet du chapitre suivant.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Le Translation Lookaside Buffer
| prevText=Le Translation Lookaside Buffer
| next=La prédiction de branchement
| nextText=La prédiction de branchement
}}
</noinclude>
835gnwrwitiiqx85xrlzki8yiaxgksw
Fonctionnement d'un ordinateur/Les bus électroniques
0
72818
774573
768678
2026-10-09T23:38:54Z
Mewtow
31375
774573
wikitext
text/x-wiki
Il y a quelques chapitres, nous avons vu la différence entre bus et liaison point à point : là où ces dernières ne connectent que deux composants, les bus de communication en connectent bien plus. Ce faisant, les bus de communication font face à de nouveaux problèmes, inconnus des liaisons point à point. Et ce sont ces problèmes qui font l'objet de ce chapitre. Autant le chapitre précédent valait à la fois pour les liaisons point à point et les bus, autant ce n'est pas le cas de celui-ci. Ce chapitre va parler de ce qui n'est valable que pour les bus de communication, comme leur arbitrage, la détection des collisions, etc. Tous ces problèmes ne peuvent pas survenir, par définition, sur les liaisons point à point.
==L'adressage du récepteur==
[[File:Bus general schematic.svg|vignette|Schéma d'un bus.]]
La trame doit naturellement être envoyée à un récepteur, seul destinataire de la trame. Sur les liaisons point à point, il n'y a pas besoin de préciser quel est le récepteur. Mais sur les bus, c'est une toute autre histoire. Tous les composants reliés aux bus sont de potentiels récepteurs et l'émetteur doit préciser à qui la trame est destinée. Pour résoudre ce problème, chaque composant se voit attribuer une '''adresse''', il est « numéroté ». Cela fonctionne aussi pour les composants qui sont des périphériques.
===L'adressage sur les bus parallèles et série===
Sur les bus parallèles, l'adresse est généralement transmise sur des fils à part, sur un sous-bus dédié appelé le bus d'adresse. En général, les adresses sur les bus pour périphériques sont assez petites, de quelques bits dans le cas le plus fréquent, quelques octets tout au plus. Il n'y a pas besoin de plus pour adresser une centaine de composants ou plus. Les seuls bus à avoir des adresses de plusieurs octets sont les bus liés aux mémoires, ou ceux qui ont un rapport avec les réseaux informatiques.
Les '''bus multiplexés''' utilisent une astuce pour économiser des fils et des broches. Un bus multiplexé sert alternativement de bus de donnée ou d'adresse, suivant la valeur d'un bit du bus de commande. Ce dernier, le bit ''Adress Line Enable'' (ALE), précise si le contenu du bus est une adresse ou une donnée : il vaut 1 quand une adresse transite sur le bus, et 0 si le bus contient une donnée.
Un défaut de ces bus est que les transferts sont plus lents, car l'adresse et la donnée ne sont pas envoyées en même temps lors d'une écriture. Un autre problème des bus multiplexé est qu'ils ont a peu près autant de bits pour coder l'adresse que pour transporter les données. Par exemple, un bus multiplexé de 8 bits transmettra des adresses de 16 bits, mais aussi des données de 16 bits. Ils sont donc moins versatiles, mais cela pose problème sur les bus où l'on peut connecter peu de périphériques. Dans ce cas, les adresses sont très petites et l'économie de fils est donc beaucoup plus faible.
Passons maintenant aux '''bus série''' (ou certains bus parallèles particuliers). Pour arriver à destination, la trame doit indiquer l'adresse du composant de destination. Les récepteurs espionnent le bus en permanence pour détecter les trames qui leur sont destinées. Ils lisent toutes les trames envoyées sur le bus et en extraient l'adresse de destination : si celle-ci leur correspond, ils lisent le reste de la trame, ils ne la prennent pas en compte sinon.
L'adresse en question est intégrée à la trame et est placée à un endroit précis, toujours le même, pour que le récepteur puisse l'extraire. Le plus souvent, l'adresse de destination est placée au début de la trame, afin qu'elle soit envoyée au plus vite. Ainsi, les périphériques savent plus rapidement si la trame leur est destinée ou non, l'adresse étant connue le plus tôt possible.
===Le décodage d'adresse===
Le fait d'attribuer une adresse à chaque composant est une idée simple, mais efficace. Encore faut-il la mettre en œuvre et il existe plusieurs possibilités pour cela. Implémenter l'adressage sur un bus demande à ce que chaque composant sache d'une manière ou d'une autre que c'est à lui que l'on veut parler et pas à un autre. Lorsqu'une adresse est envoyée sur le bus, seul l'émetteur et le récepteur se connectent au bus, les autres composants ne sont pas censés réagir. Et pour cela, il existe deux possibilités : soit on délègue l'adressage au composant, soit on ajoute un circuit qui active le composant adressé et désactive les autres.
Avec la première méthode, les composants branchés sur le bus monitorent en permanence ce qui est transféré sur le bus. Quand un envoi de commande a lieu, chaque composant extrait l'adresse transmise sur le bus et vérifie si c'est bien la sienne. Si c'est le cas, le composant se connecte sur le bus et les autres composants se déconnectent. En conséquence, chaque composant contient un comparateur pour cette vérification d'adresse, dont la sortie commande les circuits trois états qui relient le contrôleur au bus. Cette méthode est particulièrement pratique sur les bus où le bus d'adresse est séparé du bus de données. Si ce n'est pas le cas, le composant doit mémoriser l'adresse transmise sur le bus dans un registre, avant de faire la comparaison? Même chose sur les bus série.
La seconde solution est celle du '''décodage d'adresse'''. Elle utilise un circuit qui détermine, à partir de l'adresse, quel est le composant adressé. Seul ce composant sera activé/connecté au bus, tandis que les autres seront désactivés/déconnectés du bus. Pour implémenter la dernière solution, chaque périphérique possède une entrée CS, qui active ou désactive le composant suivant sa valeur. Le composant se déconnecte du bus si ce bit est à 0 et est connecté s'il est à 1. Pour éviter les conflits, un seul composant doit avoir son bit CS à 1. Pour cela, il faut ajouter un circuit qui prend en entrée l'adresse et qui commande les bits CS : ce circuit est un circuit de décodage partiel d'adresse.
[[File:Décodage d'adresse sur un bus.png|centre|vignette|upright=2|Décodage d'adresse sur un bus]]
==L'interfaçage avec le bus==
Une fois que l'on sait quel composant a accès au bus à un instant donné, il faut trouver un moyen pour que les composants non sélectionnés par l'arbitrage ne puissent pas écrire sur le bus.
Une première solution consiste à relier les entrées/sorties des composants au bus via un multiplexeur/démultiplexeur : on est alors certain que seul un composant pourra émettre sur le bus à un moment donné. L'arbitrage du bus choisit quel composant peut émettre, et configure l'entrée de commande du multiplexeur en fonction. Les multiplexeurs et démultiplexeurs sont configurés en utilisant l'adresse du composant émetteur/récepteur.
Une autre solution consiste à connecter et déconnecter les circuits du bus selon les besoins. À un instant t, seul l'émetteur et le récepteur sont connectés au bus. Mais cela demande pouvoir déconnecter du bus les entrées/sorties qui n'envoient pas de données. Plus précisément, leurs sorties peuvent être mises dans un état de haute impédance, qui n'est ni un 0 ni un 1. Quand une sortie est en haute impédance, elle n'a pas la moindre influence sur le bus et ne peut donc pas y écrire. Tout se passe comme si elle était déconnectée du bus, et dans les faits, elle l'est souvent.
Dans le chapitre sur les circuits intégrés, nous avons vu qu'il existait trois types de sorties : les sorties totem-pole, à drain/collecteur ouvert, et trois-état. Les sorties totem-pole fournissent soit un 1, soit un zéro, et ne peuvent pas être déconnectées proprement dit. Les deux autres types de sorties en sont capables. Et nous allons les voir dans ce qui suit.
===L'interfaçage avec le bus avec des circuits trois-états===
Le cas le plus simple est celui des sorties trois-état, qui peuvent soit fournir un 1, soit fournir un 0, soit être déconnectées. Malheureusement, les circuits intégrés normaux n'ont pas naturellement des entrées-sorties trois-état. Les portes logiques fournissent soit un 0, soit un 1, pas d'état déconnecté.
[[File:Tampons3E.png|vignette|Tampons 3 états.]]
La solution retenue sur presque tous les circuits actuels est d'utiliser des '''tampons trois états'''. Pour rappel, nous avions vu ce circuit dans le chapitre sur les circuits intégrés, mais un rappel ne fera clairement pas de mal. Un tampon trois-états peut être vu comme une porte OUI modifiée, qui peut déconnecter sa sortie de son entrée. Un tampon trois-état possède une entrée de donnée, une entrée de commande, et une sortie : suivant ce qui est mis sur l'entrée de commande, la sortie est soit en état de haute impédance (déconnectée du bus), soit égale à l'entrée.
{|class="wikitable"
|-
!Commande!!Entrée!!Sortie
|-
||0||0||Haute impédance/Déconnexion
|-
||0||1||Haute impédance/Déconnexion
|-
||1||0||0
|-
||1||1||1
|}
[[File:Tristate buffer corrected.svg|centre|vignette|upright=1.5|Tampon trois-états.]]
On peut utiliser ces tampons trois états pour permettre à un composant d'émettre ou de recevoir des données sur un bus. Par exemple, on peut utiliser ces tampons pour autoriser les émissions sur le bus, le composant étant déconnecté (haute impédance) s'il n'a rien à émettre. Le composant a accès au bus en écriture seule. L'exemple typique est celui d'une mémoire ROM reliée à un bus de données.
[[File:Bus en écriture seule.png|centre|vignette|upright=1.5|Bus en écriture seule.]]
Une autre possibilité est de permettre à un composant de recevoir des données sur le bus. Le composant peut alors surveiller le bus et regarder si des données lui sont transmises, ou se déconnecter du bus. Le composant a alors accès au bus en lecture seule.
[[File:Bus en lecture seule.png|centre|vignette|upright=1.5|Bus en lecture seule.]]
Évidemment, on peut autoriser lectures et écritures : le composant peut alors aussi bien émettre que recevoir des données sur le bus quand il s'y connecte. On doit alors utiliser deux circuits trois états, un pour l'émission/écriture et un autre pour la réception/lecture. Comme exemple, on pourrait citer les mémoires RAM, qui sont reliées au bus mémoire par des circuits de ce genre. Dans ce cas, les circuits trois états doivent être commandés par le bit CS (''Chip Select'') qui connecte ou déconnecte la mémoire du bus, mais aussi par le bit R/W (''Read/Write'') qui décide du sens de transfert. Pour faire la traduction entre ces deux bits et les bits à placer sur l'entrée de commande des circuits trois états, on utilise un petit circuit combinatoire assez simple.
[[File:Bus en lecture et écriture.png|centre|vignette|upright=2|Bus en lecture et écriture.]]
===L'interfaçage avec le bus avec des circuits à drain/collecteur ouvert===
Les sorties à drain/collecteur ouvert sont plus limitées et ne peuvent prendre que deux états. Dans le cas le plus fréquent, la sortie est soit déconnectée, soit mise à 0 par le circuit intégré, mais elle ne peut pas être mise à 1 sans intervention extérieure. Pour compenser cela, le bus est relié à la tension d'alimentation à travers une résistance, appelée résistance de rappel. Cela garantit que le bus est naturellement à l'état 1, du moins tant que les sorties des composants sont déconnectées. Au repos, quand les composants n’envoient rien sur le bus, les sorties des composants sont déconnectées et les résistances de rappel mettent le bus à 1. Mais quand un seul composant met sa sortie à 0, cela force le bus à passer à 0.
[[File:Open-Collector-Prinzip.JPG|centre|vignette|upright=2|Exemple de bus n'utilisant que des composants à sortie en collecteur ouvert.]]
Pour le dire autrement, on peut voir le contenu du bus comme un ET des bits envoyés sur les sorties des composants connectés au bus. Ce détail aura son importance par la suite. Le contenu du fil peut être lu sans altérer l'état électrique du bus/fil.
Avec cette méthode, le nombre de composants que l'on peut placer sur le bus est surtout limité par les spécifications électriques du bus, notamment sa capacité. Mais cela a l'avantage que le bus est compatible avec des technologies de fabrication totalement différentes, qu'il s'agisse de composants TTL, CMOS ou autres. En effet, la tension d'alimentation des composants TTL n'est pas la même que celle des composants CMOS. Utiliser des entrées-sorties à drain ouvert fait que l'on peut choisir la tension d'alimentation que l'on veut, et donc que l'on peut choisir entre TTL et CMOS. Par contre, on ne peut pas connecter composants TTL et CMOS avec des tensions d'alimentation différentes sur un même bus.
Il est possible de mélanger sorties à drain/collecteur ouvert, avec des entrées "trois-états" (des entrées qui peuvent soit permettre une lecture du bus, soit être déconnectées). C'est par exemple le cas sur les microprocesseurs 8051.
[[File:Port8051.png|centre|vignette|upright=2|Port d'un 8051]]
==L'arbitrage du bus==
Si plusieurs composants tentent d'envoyer une donnée sur le bus en même temps : c'est un '''conflit d'accès au bus'''. Les conflits d'accès au bus surviennent sur la majeure partie des bus, qu'ils soient multiplexés ou non. Sur les bus multiplexés, qui relient plus de deux composants, cette situation est fréquente du fait du nombre de récepteurs/émetteurs potentiels. Mais cela peut aussi arriver, les bus ''half-duplex'' étant des exemples particuliers : il se peut que les deux composants veuillent être émetteurs en même temps, ou récepteurs en même temps.
Et de tels conflits sont censés être évités d'une manière ou d'une autre, vu qu'un bus ne permet pas plusieurs transferts simultanés dans le même sens. Les concepteurs de bus ont inventé des méthodes pour éviter ces conflits d’accès, et choisir le plus efficacement possible l’émetteur : on parle d''''arbitrage du bus'''. L'arbitrage du bus implique qu'il faut répartir l'accès au bus pour n'avoir qu'un émetteur à la fois. On doit choisir un émetteur parmi plusieurs candidats à l'émission. Ce choix sera effectué différemment suivant le protocole du bus et son organisation, mais ce choix n’est pas gratuit. Un seul candidat sera choisit, et les autres devront attendre leur tour pour avoir accès au bus.
===Les différents types d'arbitrage de bus===
Il existe plusieurs méthodes d'arbitrages, qui peuvent se classer en différents types, selon leur fonctionnement. La première distingue l'arbitrage centralisé de l'arbitrage décentralisé. La seconde distingue l'arbitrage à priorité et à partage égal. Les deux catégories interagissent l'une avec l'autre, comme on le verra dans ce qui suit.
Une première classification distingue l'arbitrage à part égal de l'arbitrage à priorité. La différence est que le second donne la priorité à certains composants sur les autres, alors que l'arbitrage à part égales ne donne aucun traitement de faveur.
L''''arbitrage à part égales''' peut se résumer en une phrase : chacun son tour ! Chaque composant a accès au bus à tour de rôle. Cette méthode fort simple convient si les différents composants ont des besoins approximativement équilibrés. Mais elle n'est pas adaptée quand certains composants effectuent beaucoup de transactions que les autres. Les composants gourmands manqueront de débit, alors que les autres monopoliseront le bus pour ne presque rien en faire.
A l'opposé, l''''arbitrage à priorité''' donne la priorité à certains composants sur les autres. Tous les composants se voient attribuer une priorité, un nombre qui indique s'ils sont prioritaire sur les autres. Plus ce nombre est élevé, plus la priorité est importante (ou inversement). Lorsque plusieurs composants veulent accéder au bus, celui qui a la priorité la plus haute passe devant tous les autres. Certaines méthodes d'arbitrage permettent de libérer le bus de force pour laisser la place à un autre composant. On parle alors de '''''bus mastering'''''. L'idée est de libèrer le bus de force si un composant plus prioritaire veut utiliser le bus.
: Les méthodes d'arbitrage à priorité sont nombreuses, alors qu'il n'y a pas 36 façons d'implémenter l'arbitrage à parts égales.
Les deux ne sont pas utilisés dans les mêmes circonstances. Par exemple, imaginons un bus qui relie plusieurs processeurs/cœurs identiques à la mémoire RAM. Un tel arrangement est appelé un bus mémoire partagé (sous-entendu, entre plusieurs processeurs/cœurs). L'arbitrage n'a aucun raison de prioriser un processeur sur un autre, vu qu'ils sont identiques. En conséquence, l'arbitrage à parts égales est parfaitement adapté. Pour un bus système connecté à un processeur rapide, une mémoire et des entrées-sorties lentes, il vaut mieux privilégier le processeur sur les entrées-sorties. Un arbitrage à priorité est alors utilisé.
Une seconde classification nous dit si un composant gère le bus, ou si cet arbitrage est délégué aux composants qui accèdent au bus.
* Dans l''''arbitrage centralisé''', un circuit spécialisé s'occupe de l'arbitrage du bus.
* Dans l''''arbitrage distribué''', chaque composant se débrouille de concert avec tous les autres pour éviter les conflits d’accès au bus : chaque composant décide seul d'émettre ou pas, suivant l'état du bus.
: ''Notons qu'un même algorithme peut être implémenté soit de manière centralisée, soit de manière décentralisée.''
Avec l'arbitrage centralisé, l'arbitrage du bus est géré par un '''contrôleur de bus'''. Il s'agit d'un circuit qui prend en charge tout ce qui a trait au bus, au minimum l'arbitrage du bus, souvent d'autres fonctions en plus. Il communique avec les composants branchés sur le bus, dans les deux sens. Il reçoit des signaux en provenance des composants connectés au bus, et leur envoie des signaux de commande. Il est typiquement soudé à la carte mère et est surtout utilisé avec des bus système ou des bus présents sur la carte mère.
Sur les premiers PC, le contrôleurs de bus et le circuit d’arbitrage étaient séparés. Il étaient dans deux circuits imprimés différents, tous deux soudés sur la carte mère. Le contrôleur de bus était un Intel 8288, alors que le circuit d'arbitrage était un 8289 d'Intel. Les deux étaient conçus pour fonctionner en tandem, mais ils étaient séparés pour des raisons de budget en transistor. De nos jours, les deux composants sont fusionnés dans le ''chipset'' de la carte mère.
L''''arbitrage décentralisé''' est très rarement utilisé. Les exemples les plus connus sont les bus CAN et I²c, qui sont certes très utilisés. Mais la majorité des bus n'utilisent pas cet arbitrage décentralisé. La plupart des bus à arbitrage décentralisé utilisent des circuits imprimés spécifiques, qui ont des sorties à collecteur ouvert. L'arbitrage décentralisé se base sur les propriétés électriques de ce collecteur ouvert, comme on le verra plus bas. Il s'accompagne toujours d'un système de priorité.
{|class="wikitable"
|-
! !! Arbitrage centralisé !! Arbitrage décentralisé
|-
! Arbitrage à priorité
| Possible, courant. || Systématique.
|-
! Arbitrage à parts égales
| Possible, peu courant. || Impossible.
|}
Dans ce qui va suivre, nous allons voir diverses méthodes d'arbitrage de bus. Nous allons surtout voir des méthodes centralisées, car elles sont nettement plus simples à comprendre. Sauf exception, elles fonctionnent toutes sur le même principe. Les composants envoient une requête au contrôleur de bus, pour demander l'accès au bus. Le contrôleur de bus analyse les requêtes et décide quel composant a l'accès exclusif au bus, à un instant donné. Le composant en question est appelé le '''''bus master''''', ou maitre du bus. Pour résumer, le contrôleur de bus élit le ''bus master'' parmi une liste de candidats, qui ont demandé une requête d'accès au bus.
La sélection du ''bus master'' varie d'un bus à l'autre. Certains bus donnent l'accès à chaque candidat à part égale, d'autres ont un système de priorité où certains composants passent avant les autres.
===L'arbitrage centralisé à requêtes indépendantes===
L''''arbitrage par requêtes indépendantes''' est le plus simple à comprendre, car il applique ce principe de requête et de sélection à la lettre. Chaque composant connecté au bus est connecté au contrôleur de bus, avec une liaison point à point. Chaque composant peut donc candidater pour accéder au bus indépendamment des autres. Et le contrôleur de bus lui communique sa réponse par sa propre liaison point à point.
Dans le détail, les composants sont reliés au contrôleur de bus avec deux fils, chacun transmettant un bit : un fil ''Request'' et un fil ''Grant''. Le bit ''Request'' permet au composant d'envoyer une requête d'accès au bus : il est mis à 1 quand le composant veut accéder au bus. Il est donc connecté à une sortie du composant, mais est une entrée pour le contrôleur de bus. Le bit ''Grant'' est généré par le contrôleur de bus et est envoyé au composant élu, celui auquel il donne l'accès au bus. S'il est à 1, le composant qui reçoit ce signal se connecte au bus, les autres attendent leur tour.
[[File:Arbitrage centralisé à requêtes indépendantes.png|centre|vignette|upright=2|Arbitrage centralisé à requêtes indépendantes.]]
Vous avez sans doute pensé à un détail : que ce se passe-t-il quand le bus est occupé ? Est-ce que les composants sont au courant que le bus est occupé ou non ? La réponse dépend du bus et de la méthode d'arbitrage, mais il y a deux grandes réponses principales. Mais en général, un fil du bus de commande précise si le bus est libre. Il s'agit du bit '''''Busy''''', qui indique si le bus est libre ou occupé.
Quand un composant reçoit l'autorisation du contrôleur de bus, il met ce fil à 1, ce qui indique au contrôleur de bus que le bus est occupé. Il maintient ce fil à 1 tant qu'il utilise le bus, et le met à zéro quand il libère le bus. Le contrôleur de bus sait donc quand le bus est libre ou occupé, et décide de l'arbitrage en fonction. Pour cela, chaque composant a une sortie ''Busy'', qui indique qu'il utilise le bus. Le fil ''Busy'' est obtenu en faisant un OU entre les sorties ''Busy'' de chaque composant. Le OU en question est réalisé soit avec une chaine de portes OU, soit avec un OU câblé.
[[File:Busy bit for bus arbitration.png|centre|vignette|upright=2|''Busy'' bit pour l'abitrage du bus.]]
La méthode d'arbitrage dépend du contrôleur de bus utilisé. Il est possible d'utiliser un microcontroleur comme contrôleur de bus, ce qui permet de programmer l’algorithme d'arbitrage. Mais il est aussi possible d'utiliser un circuit dédié. L'arbitrage par requêtes indépendante peut implémenter l'arbitrage à part égale, comme un arbitrage à priorité. L'arbitrage à part égale est implémenté avec un simple compteur couplé à un décodeur. Le compteur indique quel composant a accès au bus, le décodeur traduit ce nombre en signaux ''Grant''. Le compteur est incrémenté lorsqu'un composant libère le bus.
Pour l'arbitrage à priorité, il est possible de l'implémenter avec un encodeur à priorité, dont les entrées sont connectées au signaux ''Request''. Le résultat de l'encodeur à priorité passe dans un décodeur, ce qui donne les signaux ''Grant'' adéquats. Bien sur, il y a d'autres circuits autour, qui font en sorte que l'envoi des signaux ''Grant'' se fasse seulement quand le bus est libéré, et bien d'autres choses.
[[File:Arbitrage de bus centralisé, à résolution parallèle.png|centre|vignette|upright=2|Arbitrage de bus centralisé, à résolution parallèle]]
Un défaut de cette solution est qu'elle demande de câbler beaucoup de fils. Aussi, les autres méthodes procèdent autrement, afin d'économiser des fils. Les deux méthodes que nous allons voir mutualisent certains fils entre plusieurs composants. Par exemple, avec la méthode du ''pooling'', il n'y a qu'un seul fil ''Request'' partagé entre tous les composants. Et avec la ''Daisy chain'', cela va plus loin car tous les fils ''Grant'' sont mutualisés.
===L'arbitrage centralisé par adressage===
Dans la section précédente, le contrôleur de bus doit utiliser N sorties ''Grant'' pour sélectionner le composant adéquat. En contrepartie, chaque composant n'a qu'une seule entrée ''Grant''. Une solution intermédiaire réduit le nombre de signaux ''Grant'', au prix d'une augmentation des entrées sur chaque composant. Mais au total, le nombre de fils est grandement réduit.
L'idée est de remplacer les signaux ''Grant'' par l'adresse du composant sélectionné. Avec ce système, tous les composants ont une adresse qui permet de les sélectionner. Il est possible de configurer cette adresse pour chaque composant, à savoir que chaque composant mémorise son adresse dans un registre. Il y a un bus d'adresse séparé pour l'arbitrage, qui est commandé par le contrôleur de bus. Le contrôleur de bus envoie l'adresse du ''bus master'' sur ce bus. Les composants lisent cette adresse et détectent s'il s'agit de la leur. Si l'adresse ne lui est pas destiné, le composant sait qu'il n'y a pas accès au bus. Mais si c'est le cas, le composant sait qu'il est sélectionné et se connecte au bus.
Au final, au lieu d'avoir N fils ''Grant'' pour N composants, le bus d'adresse fait beaucoup moins. Par exemple, pour 16 composants, on passe de 16 fils à 4. Le contrôleur de bus économise donc beaucoup de broches. Mais cela se fait en ajoutant des broches sur les composants, sans compter que ceux-ci doivent mémoriser leur adresse dans un registre. Mais rien d'insurmontable, le cout en circuits est très faible.
: Notons qu'un tel arbitrage se marie assez bien avec le ''Direct Memory Access''.
===L'arbitrage centralisé à ''daisy chain''===
L''''arbitrage par ''daisy chain''''' est un algorithme centralisé, dans lequel tout composant a une priorité fixe. Dans celui-ci, tous les composants sont reliés à un arbitre, qui dit si l'accès au bus est autorisé. Cependant, le système utilise encore moins de fils que les arbitrages précédents. L'arbitre du bus n'a qu'une seule entrée ''Request'', et une sortie ''Grant''. Il y a aussi un fil ''Busy'' qui indique si le bus est libre ou non.
Même si le contrôleur de bus n'a qu'une seule entrée ''Request'', les composants ont tous une sortie ''Request''. L'idée est que l'entrée ''Request'' du contrôleur de bus est un OU entre toutes les sorties ''Request''. Le contrôleur de bus sait donc qu'un composant a demandé l'accès au bus, mais il ne sait pas lequel. Et la ''Daisy Chain'' fait qu'il n'a pas à le savoir. Le OU entre les sorties ''Request'' peut se faire de plusieurs manières. Soit avec une chaine de portes OU, soit avec un OU cablé. Le signal ''Busy'' est lui aussi généré de la même manière.
[[File:Implémentation du OU du fil REQUEST dans une Daisy chain.png|centre|vignette|upright=2|Implémentation du OU du fil REQUEST dans une Daisy chain.]]
Le signal ''Grant'' est propagé d'un composant à l'autre, jusqu'à atteindre le premier composant à avoir demandé d'accès au bus. Pour cela, les composants sont reliés en guirlande, à savoir que chacun prend en entrée un signal ''Grant'' et fournit un second signal ''Grant'' sur une sortie du même nom. Si le composant n'a pas demandé l'accès au bus, il recopie le signal ''Grant'' en entrée sur sa sortie, sans le modifier. Mais s'il veut accéder au bus, il force sa sortie ''Grant'' à zéro : les composants suivants verront ainsi un 1 sur le fil, mais les suivants verront un zéro (interdiction d'accès). Ainsi, les composants les plus près du bus, dans l'ordre de la guirlande, seront prioritaires sur les autres.
[[File:Decentralizz.jpg|centre|vignette|upright=2|Daisy Chain.]]
Un composant utilise le signal ''Grant'' en entrée pour deux choses : savoir s'il a accès au bus, calculer le signal ''Grant'' en sortie. Pour ces deux opérations, le composant combine le signal ''Grant'' d'entrée avec le signal ''Request''. Le composant a accès au bus quand : il a demandé l'accès au bus, le signal ''Grant'' lui accorde l'accès. La première condition correspond à un signal ''Request'' à 1, la seconde à un signal ''Grant'' à 1. Une vulgaire porte ET fait l'affaire. Pour générer le signal ''Grant'' en sortie, il faut établir sa table de vérité, et il s’avère qu'il s'agit d'une porte XOR entre les deux signaux.
[[File:Implémentation de la Daisy chain dans les émeteurs et récepteurs.png|centre|vignette|upright=2|Implémentation de la Daisy chain dans les émeteurs et récepteurs]]
===L'arbitrage décentralisé sur les bus à collecteur ouvert===
Les bus à collecteur ouvert ont un avantage pour ce qui est de l'arbitrage : ils permettent de détecter les collisions assez simplement. En effet, le contenu du bus est égal à un ET entre toutes les sorties reliées au bus. Si tous les composants veulent laisser le bus à 1 à un instant t, le bus sera à 1 : s'il y a collision, elle n'est pas grave car tous les composants envoient la même chose. Pareil s'ils veulent tous mettre le bus à 0 : le bus sera à 0 et la collision n'aura aucun impact. Par contre, si une sortie veut mettre le bus à 0 et un autre veut le laisser à 1, alors le bus sera mis à 0.
La détection des collisions est alors évidente. Les composants qui émettent quelque chose sur le bus vérifient si le bus a bien la valeur qu'ils envoient dessus. Si les deux concordent, on ne sait pas il y a collision et il y a de bonnes chances que ce ne soit pas le cas, alors on continue la transmission. Mais si un composant envoie un 1 et que le bus est à 0, cela signifie qu'un autre composant a mis le bus à 0 et qu'il y a une collision. Le composant qui a détecté la collision cesse immédiatement la transmission et laisse la place au composant qui a mis le bus à 0, il le laisse finir la transmission entamée.
Un exemple de ce genre est celui du bus CAN, très utilisé dans le domaine automobile. Lorsque le bus est libéré, les composants qui veulent accéder au bus envoient leur adresse sur le bus. La trame CAN est conçue de manière à ce que l'adresse de l'émetteur est transmise en premier. Le bus étant à collecteur ouvert, plusieurs composants pourront envoyer leur adresse sur ce bus en même temps. Tant que les bits d'adresse transmis sont identiques, il n'y a pas de collision. Par contre, le premier composant à forcer le bus à zéro gagnera la compétition et continuera à transmettre son adresse. Les autres auront perdu d'arbitrage. La conséquence est que l'arbitrage utilise l'adresse pour déterminer la priorité. Plus l'adresse d'un composant sera petite, plus il sera prioritaire sur les autres.
[[File:Exemple d'arbitrage CAN.png|centre|vignette|upright=2|Exemple d'arbitrage CAN]]
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=Les liaisons point à point
| prevText=Les liaisons point à point
| next=Quelques exemples de bus et de liaisons point à point
| nextText=Quelques exemples de bus et de liaisons point à point
}}{{autocat}}
</noinclude>
adulramyjom38mwgxnzmfxs5e3agb1o
Mathc initiation/a594
0
81090
774484
774471
2026-10-09T14:41:17Z
Xhungab
23827
774484
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a522#Analyse IV : Se familiariser avec les séries de Fourier|Sommaire]]
[[File:Fourier serie for my wikibook.png|thumb|Fourier serie f(x) = x with x[0..Pi]]]
'''L'étude de ce chapitre peut ce faire à l'aide de cette [[https://youtube.com/playlist?list=PLi6peGpf8EPOQGZMpyjDEHjnf1wsQbDJG&si=mhV8SOb34ohQKvKm Playlist]].'''
En analyse mathématique, les séries de Fourier sont un outil fondamental dans l'étude des fonctions périodiques. C'est à partir de ce concept que s'est développée la branche des mathématiques connue sous le nom d'analyse harmonique. [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier wikipedia]
.
'''Pour les définitions de a0, b0, an et bn j'utilise la [https://fr.wikipedia.org/wiki/S%C3%A9rie_de_Fourier#Coefficients_réels Première convention]'''
* <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* <math>\mathbf{b_0(f)=0}</math>
* '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* '''pour''' <math>\mathbf{n>0}</math> : <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
----
==Se familiariser avec les series de Fourier :==
Quelques exemples :
* [[Mathc initiation/a593|La fonction : f(x) = x ...................... avec x = [0..PI]]]
* [[Mathc initiation/007n|La fonction : f(x) = x^2 .................. avec x = [-PI..PI]]]
* [[Mathc initiation/a602|La fonction : f(x) = (x(x+1)) ........... avec x = [-1..1]]]
==Calculons les amplitudes An et les phases φn==
* [[Mathc initiation/a9b|Présentation]]
{{AutoCat}}
k42y2b3fnj8n9lzp1lwk49pmcmxt09f
Mathc initiation/a593
0
81091
774518
774439
2026-10-09T15:59:23Z
Xhungab
23827
774518
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les series de Fourier avec f : -> x}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/a595|f.h ]] ...... '''f(x) = x avec x = [0..PI]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/a601|c0a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/a596|c0an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/a597|c0bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
{| class="wikitable"
|+ Séries de Fourier
|-
! plot [0.:pi] [0.:pi] !! plot [0.:pi] [0.:pi]
|-
| [[File:Fourier serie for my wikibook.png|thumb|]] ||
[[File:Fourier serie for my wikibook.png|thumb|]]
|}
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/a598|c0fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/a615|c0fsN.c]]
{{AutoCat}}
5xf4puc8w3iock60zyla20ivixmv12g
774574
774518
2026-10-10T08:32:15Z
Xhungab
23827
774574
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les series de Fourier avec f : -> x}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/a595|f.h ]] ...... '''f(x) = x avec x = [0..PI]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/a601|c0a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/a596|c0an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/a597|c0bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
{| class="wikitable"
|+ Séries de Fourier
|-
! plot [0.:pi] [0.:pi] !! plot [0.:pi] [0.:pi]
|-
| [[File:Fourier serie for my wikibook.png|thumb|]] ||
[[File:Fourier serie for my wikibook.png|thumb|]]
|}
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les 9 premiers termes''' de la série.
* [[Mathc initiation/a598|c0fs.c]]
Avec ce fichier C nous allons pouvoir dessiner '''les N premiers termes''' de la série.
* [[Mathc initiation/a615|c0fsN.c]]
{{AutoCat}}
ef59d5g7z1ivq52ggbl9o887qyli0h7
Mathc initiation/a596
0
81096
774476
737546
2026-10-09T13:46:19Z
Xhungab
23827
774476
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a593| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c0an.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c0an.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f0.h"
/* ---------------------------------- */
int main(void)
{
double T = PI;
double omega = (2.*PI/T);
double a = 0.;
double b = PI;
double n = 1.; /* You can change the value of n */
double an = 2./T * fourier_an(f, a,b, omega, n);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" (a + T\n"
" a_%.0f = 2/T int( (%s) cos(%.0f omega t) dt = %.6f\n"
" (a \n\n\n\n",n, feq, n, an);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
(a + T
a_1 = 2/T int( (t) cos(1 omega t) dt = 0.000000
(a
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
gemqy21z81fqehhucoxnoiehrjfvtah
Mathc initiation/a597
0
81097
774475
737547
2026-10-09T13:45:10Z
Xhungab
23827
774475
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a593| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c0bn.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c0bn.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f0.h"
/* ---------------------------------- */
int main(void)
{
double T = PI;
double omega = (2.*PI/T);
double a = 0.;
double b = PI;
double n = 1.; /* You can change the value of n */
double bn = 2./T * fourier_bn(f, a,b, omega, n);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" (a + T\n"
" b_%.0f = 2/T int( (%s) sin(%.0f omega t) dt = %.6f\n"
" (a \n\n\n\n", n, feq, n, bn);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
(a + T
b_1 = 2/T int( (t) sin(1 omega t) dt = -1.000000
(a
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
3bf8m2gljfkeumhtg29dbhqf86m3mrx
Mathc initiation/a598
0
81098
774519
770753
2026-10-09T16:00:13Z
Xhungab
23827
774519
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a593| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c0fs.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c0fs.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f0.h"
/* ---------------------------------- */
int main(void)
{
double T = PI;
double omega = (2.*PI/T);
double a = 0.;
double b = PI;
double a0 = 1./T * fourier_a0(f, a,b);
double an = 2./T * fourier_an(f, a,b, omega, 1);
double b1 = 2./T * fourier_bn(f, a,b, omega, 1);
double b2 = 2./T * fourier_bn(f, a,b, omega, 2);
double b3 = 2./T * fourier_bn(f, a,b, omega, 3);
double b4 = 2./T * fourier_bn(f, a,b, omega, 4);
double b5 = 2./T * fourier_bn(f, a,b, omega, 5);
double b6 = 2./T * fourier_bn(f, a,b, omega, 6);
double b7 = 2./T * fourier_bn(f, a,b, omega, 7);
double b8 = 2./T * fourier_bn(f, a,b, omega, 8);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" a0 = %.6f \n",a0);
printf(" an = %.6f \n\n",an);
printf(
" %+.9f\\\n"
" %+.9f*sin(%.9f* 1*x)\\\n"
" %+.9f*sin(%.9f* 2*x)\\\n"
" %+.9f*sin(%.9f* 3*x)\\\n"
" %+.9f*sin(%.9f* 4*x)\\\n"
" %+.9f*sin(%.9f* 5*x)\\\n"
" %+.9f*sin(%.9f* 6*x)\\\n"
" %+.9f*sin(%.9f* 7*x)\\\n"
" %+.9f*sin(%.9f* 8*x)\n"
" ... \n\n\n",
a0,
b1, omega,
b2, omega,
b3, omega,
b4, omega,
b5, omega,
b6, omega,
b7, omega,
b8, omega);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 1.570796
an = 0.000000
+1.570796327\
-1.000000005*sin(2.000000000* 1*x)\
-0.500000043*sin(2.000000000* 2*x)\
-0.333333480*sin(2.000000000* 3*x)\
-0.250000347*sin(2.000000000* 4*x)\
-0.200000678*sin(2.000000000* 5*x)\
-0.166667841*sin(2.000000000* 6*x)\
-0.142859010*sin(2.000000000* 7*x)\
-0.125002792*sin(2.000000000* 8*x)
...
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
42cn4utye9hp6bbtm9j9dqt2jtqr2ij
Mathc initiation/a602
0
81099
774485
774441
2026-10-09T14:42:06Z
Xhungab
23827
774485
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les séries de Fourier avec f: -> (x(x+1))}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/a603|f1.h]] ...... '''f(x) = (x(x+1)) avec x = [-1..1]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/a604|c1a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/a605|c1an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/a606|c1bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/a607|c1fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/a616|c1fsN.c]]
{{AutoCat}}
5idbrz7e2cjqea0o1f0kwm3gk8ca0tf
774513
774485
2026-10-09T15:39:21Z
Xhungab
23827
774513
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les séries de Fourier avec f: -> (x(x+1))}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/a603|f1.h]] ...... '''f(x) = (x(x+1)) avec x = [-1..1]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/a604|c1a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/a605|c1an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/a606|c1bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
{| class="wikitable"
|+ Séries de Fourier
|-
! plot [-1:1] [-.5:2.] !! plot [-1:4] [-.5:2.]
|-
| [[File:Fourier serie in language c with gnuplot.png|thumb|]] ||
[[File:Fourier serie for my wikibook 4.png|thumb|]]
|}
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/a607|c1fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/a616|c1fsN.c]]
{{AutoCat}}
ix1ppz283thjyhff2tj1r7kwjdbcefl
774516
774513
2026-10-09T15:45:53Z
Xhungab
23827
774516
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les séries de Fourier avec f: -> (x(x+1))}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/a603|f1.h]] ...... '''f(x) = (x(x+1)) avec x = [-1..1]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/a604|c1a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/a605|c1an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/a606|c1bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
{| class="wikitable"
|+ Séries de Fourier
|-
! plot [-1:1] [-.5:2.] !! plot [-1:4] [-.5:2.]
|-
| [[File:Fourier serie in language c with gnuplot.png|thumb|]] ||
[[File:Fourier serie for my wikibook 4.png|thumb|]]
|}
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007p|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/a607|c1fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007p|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/a616|c1fsN.c]]
{{AutoCat}}
oqxlttlmd23p57lsfrgmrr8aonckb1e
774577
774516
2026-10-10T08:37:46Z
Xhungab
23827
774577
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les séries de Fourier avec f: -> (x(x+1))}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/a603|f1.h]] ...... '''f(x) = (x(x+1)) avec x = [-1..1]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/a604|c1a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/a605|c1an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/a606|c1bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
{| class="wikitable"
|+ Séries de Fourier
|-
! plot [-1:1] [-.5:2.] !! plot [-1:4] [-.5:2.]
|-
| [[File:Fourier serie in language c with gnuplot.png|thumb|]] ||
[[File:Fourier serie for my wikibook 4.png|thumb|]]
|}
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007p|gnuplot]]''' '''les 9 premiers termes''' de la série.
* [[Mathc initiation/a607|c1fs.c]]
Avec ce fichier C nous allons pouvoir dessiner '''les N premiers termes''' de la série.
* [[Mathc initiation/a616|c1fsN.c]]
{{AutoCat}}
6h616uvlkc65qupce15a83mwoh2jn45
Mathc initiation/a607
0
81104
774512
770751
2026-10-09T15:28:28Z
Xhungab
23827
774512
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a602| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c1fs.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c1fs.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f1.h"
/* ---------------------------------- */
int main(void)
{
double T = 2.;
double omega = (2.*PI/T);
double a = -1;
double b = +1;
double a0 = 1./T * fourier_a0(f, a,b);
double a1 = 2./T * fourier_an(f, a,b, omega, 1);
double a2 = 2./T * fourier_an(f, a,b, omega, 2);
double a3 = 2./T * fourier_an(f, a,b, omega, 3);
double a4 = 2./T * fourier_an(f, a,b, omega, 4);
double a5 = 2./T * fourier_an(f, a,b, omega, 5);
double a6 = 2./T * fourier_an(f, a,b, omega, 6);
double a7 = 2./T * fourier_an(f, a,b, omega, 7);
double a8 = 2./T * fourier_an(f, a,b, omega, 8);
double b1 = 2./T * fourier_bn(f, a,b, omega, 1);
double b2 = 2./T * fourier_bn(f, a,b, omega, 2);
double b3 = 2./T * fourier_bn(f, a,b, omega, 3);
double b4 = 2./T * fourier_bn(f, a,b, omega, 4);
double b5 = 2./T * fourier_bn(f, a,b, omega, 5);
double b6 = 2./T * fourier_bn(f, a,b, omega, 6);
double b7 = 2./T * fourier_bn(f, a,b, omega, 7);
double b8 = 2./T * fourier_bn(f, a,b, omega, 8);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" a0 = %.6f\n\n\n",a0);
printf(" %+.9f\\\n"
" %+.9f*cos(%.9f* 1*x) %+.9f*sin(%.9f* 1*x)\\\n"
" %+.9f*cos(%.9f* 2*x) %+.9f*sin(%.9f* 2*x)\\\n"
" %+.9f*cos(%.9f* 3*x) %+.9f*sin(%.9f* 3*x)\\\n"
" %+.9f*cos(%.9f* 4*x) %+.9f*sin(%.9f* 4*x)\\\n"
" %+.9f*cos(%.9f* 5*x) %+.9f*sin(%.9f* 5*x)\\\n"
" %+.9f*cos(%.9f* 6*x) %+.9f*sin(%.9f* 6*x)\\\n"
" %+.9f*cos(%.9f* 7*x) %+.9f*sin(%.9f* 7*x)\\\n"
" %+.9f*cos(%.9f* 8*x) %+.9f*sin(%.9f* 8*x)\n\n",
a0,
a1,omega, b1,omega,
a2,omega, b2,omega,
a3,omega, b3,omega,
a4,omega, b4,omega,
a5,omega, b5,omega,
a6,omega, b6,omega,
a7,omega, b7,omega,
a8,omega, b8,omega);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 0.333333
+0.333333333\
-0.405284728*cos(3.141592654* 1*x) +0.636619776*sin(3.141592654* 1*x)\
+0.101321157*cos(3.141592654* 2*x) -0.318309914*sin(3.141592654* 2*x)\
-0.045031578*cos(3.141592654* 3*x) +0.212206684*sin(3.141592654* 3*x)\
+0.025330190*cos(3.141592654* 4*x) -0.159155164*sin(3.141592654* 4*x)\
-0.016211224*cos(3.141592654* 5*x) +0.127324386*sin(3.141592654* 5*x)\
+0.011257671*cos(3.141592654* 6*x) -0.106104043*sin(3.141592654* 6*x)\
-0.008270792*cos(3.141592654* 7*x) +0.090946870*sin(3.141592654* 7*x)\
+0.006332148*cos(3.141592654* 8*x) -0.079579249*sin(3.141592654* 8*x)
Press return to continue.
</syntaxhighlight>
[[File:Fourier serie in language c with gnuplot.png|thumb|fourier serie in language c with gnuplot for my wikibook]]
'''Fichier de commande gnuplot :'''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-1:1] [-.5:2.]\
(x*(x+1)),\
+0.333333333\
-0.405284728*cos(3.141592654* 1*x) +0.636619776*sin(3.141592654* 1*x)\
+0.101321157*cos(3.141592654* 2*x) -0.318309914*sin(3.141592654* 2*x)\
-0.045031578*cos(3.141592654* 3*x) +0.212206684*sin(3.141592654* 3*x)\
+0.025330190*cos(3.141592654* 4*x) -0.159155164*sin(3.141592654* 4*x)\
-0.016211224*cos(3.141592654* 5*x) +0.127324386*sin(3.141592654* 5*x)\
+0.011257671*cos(3.141592654* 6*x) -0.106104043*sin(3.141592654* 6*x)\
-0.008270792*cos(3.141592654* 7*x) +0.090946870*sin(3.141592654* 7*x)\
+0.006332148*cos(3.141592654* 8*x) -0.079579249*sin(3.141592654* 8*x)
reset
</syntaxhighlight>
{{AutoCat}}
ip4luh57qn5tcmxpsybxz5q00pfgi1d
774514
774512
2026-10-09T15:41:27Z
Xhungab
23827
774514
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a602| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c1fs.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c1fs.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f1.h"
/* ---------------------------------- */
int main(void)
{
double T = 2.;
double omega = (2.*PI/T);
double a = -1;
double b = +1;
double a0 = 1./T * fourier_a0(f, a,b);
double a1 = 2./T * fourier_an(f, a,b, omega, 1);
double a2 = 2./T * fourier_an(f, a,b, omega, 2);
double a3 = 2./T * fourier_an(f, a,b, omega, 3);
double a4 = 2./T * fourier_an(f, a,b, omega, 4);
double a5 = 2./T * fourier_an(f, a,b, omega, 5);
double a6 = 2./T * fourier_an(f, a,b, omega, 6);
double a7 = 2./T * fourier_an(f, a,b, omega, 7);
double a8 = 2./T * fourier_an(f, a,b, omega, 8);
double b1 = 2./T * fourier_bn(f, a,b, omega, 1);
double b2 = 2./T * fourier_bn(f, a,b, omega, 2);
double b3 = 2./T * fourier_bn(f, a,b, omega, 3);
double b4 = 2./T * fourier_bn(f, a,b, omega, 4);
double b5 = 2./T * fourier_bn(f, a,b, omega, 5);
double b6 = 2./T * fourier_bn(f, a,b, omega, 6);
double b7 = 2./T * fourier_bn(f, a,b, omega, 7);
double b8 = 2./T * fourier_bn(f, a,b, omega, 8);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" a0 = %.6f\n\n\n",a0);
printf(" %+.9f\\\n"
" %+.9f*cos(%.9f* 1*x) %+.9f*sin(%.9f* 1*x)\\\n"
" %+.9f*cos(%.9f* 2*x) %+.9f*sin(%.9f* 2*x)\\\n"
" %+.9f*cos(%.9f* 3*x) %+.9f*sin(%.9f* 3*x)\\\n"
" %+.9f*cos(%.9f* 4*x) %+.9f*sin(%.9f* 4*x)\\\n"
" %+.9f*cos(%.9f* 5*x) %+.9f*sin(%.9f* 5*x)\\\n"
" %+.9f*cos(%.9f* 6*x) %+.9f*sin(%.9f* 6*x)\\\n"
" %+.9f*cos(%.9f* 7*x) %+.9f*sin(%.9f* 7*x)\\\n"
" %+.9f*cos(%.9f* 8*x) %+.9f*sin(%.9f* 8*x)\n\n",
a0,
a1,omega, b1,omega,
a2,omega, b2,omega,
a3,omega, b3,omega,
a4,omega, b4,omega,
a5,omega, b5,omega,
a6,omega, b6,omega,
a7,omega, b7,omega,
a8,omega, b8,omega);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 0.333333
+0.333333333\
-0.405284728*cos(3.141592654* 1*x) +0.636619776*sin(3.141592654* 1*x)\
+0.101321157*cos(3.141592654* 2*x) -0.318309914*sin(3.141592654* 2*x)\
-0.045031578*cos(3.141592654* 3*x) +0.212206684*sin(3.141592654* 3*x)\
+0.025330190*cos(3.141592654* 4*x) -0.159155164*sin(3.141592654* 4*x)\
-0.016211224*cos(3.141592654* 5*x) +0.127324386*sin(3.141592654* 5*x)\
+0.011257671*cos(3.141592654* 6*x) -0.106104043*sin(3.141592654* 6*x)\
-0.008270792*cos(3.141592654* 7*x) +0.090946870*sin(3.141592654* 7*x)\
+0.006332148*cos(3.141592654* 8*x) -0.079579249*sin(3.141592654* 8*x)
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
dujga9q4n5ofavzn626jpi91441aj8c
Mathc initiation/a615
0
81129
774520
770754
2026-10-09T16:01:03Z
Xhungab
23827
774520
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a593| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c0fsN.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c0fsN.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#define N 50
/* ---------------------------------- */
#include "f0.h"
/* ---------------------------------- */
int main(void)
{
double T = PI;
double omega = (2.*PI/T);
double a = 0.;
double b = PI;
double A[N+1];
double B[N+1];
int n = 0;
A[0] = 1./T * fourier_a0(f, a,b);
for( n=1; n<=N; n++)
B[n] = 2./T * fourier_bn(f, a,b, omega, n);
clrscrn();
printf(" %+.9f\\\n", A[0]);
for( n=1; n<N; n++)
printf(" %+.9f*sin(%+.9f* %d*x)\\\n", B[n], omega, n);
printf(" %+.9f*sin(%+.9f* %d*x)\n\n\n", B[n], omega, n);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
+1.570796327\
-1.000000005*sin(2.000000000* 1*x)\
-0.500000043*sin(2.000000000* 2*x)\
-0.333333480*sin(2.000000000* 3*x)\
...
-0.021644202*sin(2.000000000* 48*x)\
-0.021283386*sin(2.000000000* 49*x)\
-0.020943951*sin(2.000000000* 50*x)
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
tl6n9hmf8izxvmx20qwyol6nd9iw7j0
Mathc initiation/a616
0
81130
774515
770752
2026-10-09T15:42:02Z
Xhungab
23827
774515
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a602| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c1fsN.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c1fsN.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#define N 50
/* ---------------------------------- */
#include "f1.h"
/* ---------------------------------- */
int main(void)
{
double T = 2.;
double omega = (2.*PI/T);
double a = -1;
double b = +1;
double A[N+1];
double B[N+1];
int n = 0;
A[0] = 1./T * fourier_a0(f, a,b);
for( n=1; n<=N; n++)
{
A[n] = 2./T * fourier_an(f, a,b, omega, n);
B[n] = 2./T * fourier_bn(f, a,b, omega, n);
}
clrscrn();
printf(" %+.9f\\\n", A[0]);
for( n=1; n<N; n++)
printf(" %+.9f*cos(%.9f* %d*x) %+.9f*sin(%.9f* %d*x)\\\n",
A[n], omega, n,
B[n], omega, n);
printf(" %+.9f*cos(%.9f* %d*x) %+.9f*sin(%.9f* %d*x)\n",
A[n], omega, n,
B[n], omega, n);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="c">
+0.333333333\
-0.405284728*cos(3.141592654* 1*x) +0.636619776*sin(3.141592654* 1*x)\
+0.101321157*cos(3.141592654* 2*x) -0.318309914*sin(3.141592654* 2*x)\
-0.045031578*cos(3.141592654* 3*x) +0.212206684*sin(3.141592654* 3*x)\
...
+0.000150672*cos(3.141592654* 48*x) -0.013779127*sin(3.141592654* 48*x)\
-0.000141849*cos(3.141592654* 49*x) +0.013549424*sin(3.141592654* 49*x)\
+0.000133333*cos(3.141592654* 50*x) -0.013333333*sin(3.141592654* 50*x)
</syntaxhighlight>
{{AutoCat}}
5hyx9pbq7q35j3h5dwsxk6pah5hbshp
Les cartes graphiques/Les accélérateurs de scanline
0
82283
774569
774452
2026-10-09T22:31:14Z
Mewtow
31375
774569
wikitext
text/x-wiki
Les chapitres précédents ont abordé des VDC couplés à un ''framebuffer'', mais tous les VDC ne sont pas dans ce cas. La raison est assez simple : une image prend beaucoup de mémoire ! Par exemple, prenons le cas d'une image d'une résolution de 320 par 240 pixels, où chaque pixel est codé sur un octet. L'image prend alors 76800 octets, soit environ 76 kiloctets. Mine de rien, les tout premiers ordinateurs n'avaient pas assez de mémoire vidéo pour ça.
Il existe plusieurs optimisations pour économiser de la mémoire, comme le rendu en tiles que nous verrons dans le prochain chapitre. Et l'une d'entre elle rend l'image ligne par ligne. La mémoire vidéo ne contient que la ligne en cours de rendu, pas une image complète. La mémoire vidéo est alors appelée le ''line buffer'', '''tampon de ligne''' en français. Il n'y a pas de terminologie officielle pour désigner de tels VDC, mais nous pouvons les appeler des '''accélérateurs de ''scanline'''''. Les premières consoles ATARI utilisaient des accélérateurs de ''scanline'', de même que la carte graphique de la console néo-géo.
Avec un accélérateur de ''scanline'' basique, le processeur écrit la prochaine ligne à afficher dans le tampon de ligne. Les accélérateurs de ''scanline'' peuvent supporter les ''sprites'' matériels. Typiquement, ils supportent un certain nombre de ''sprite'' par ligne. Notez le "par ligne". En effet, le processeur peut en profiter pour changer les ''sprites'' entre l'affichage de deux lignes.
Un accélérateur de ''scanline'' peut gérer nativement le ''double buffering'', avec deux tampons de ligne. L'un est utilisé par le VDC pour afficher une ligne, le second reçoit la ligne en cours de calcul. Mais il s'agit là des accélérateurs de '''scanline'' les plus complexes, d'autres n'avaient qu'un seul tampon de ligne. Le processeur modifiait le tampon de ligne entre l'affichage de deux images, avec un timing assez serré, pendant l'intervalle de ''horizontal blank''. Il s'agit d'une technique appelée le '''''racing the beam'''''. Pour cela, le VDC émet une interruption matérielle à la fin de l’affichage de la ligne courante.
==La ''display list''==
Les accélérateurs de ''scanline'' les plus complexes permettent de réduire grandement le nombre de ''raster interrupts'' en intégrant une sorte de contrôleur DMA programmable. Dans le détail, ce contrôleur DMA exécute un programme composé d'une suite d'instructions simples, chacune commande l'affichage d'une ligne à l'écran. Le programme exécuté est une série d'instructions appelée la '''''display list''''', terme qui reviendra dans quelques chapitres.
La ''display list'' commence par un préambule de configuration, qui indique la résolution, le mode graphique, etc. Puis, elle afficher l'image à l'écran, ligne par ligne, sans intervention du processeur. La display list a une instruction par ligne à afficher, l'instruction copie la ligne à afficher de la mémoire RAM principale vers la mémoire vidéo (le tampon de ligne), pendant une période de ''blanking'' horizontal. La ''display list'' s'occupe de séquencer les lignes une par une, sans intervention du processeur. Le processeur n'a pas besoin de copier lui-même chaque ligne, la ''display list'' le fait automatiquement pour toutes les lignes.
Un avantage est que le défilement vertical est très rapide avec une ''display list''. Il suffit de rajouter ou d'enlever des lignes dans la ''display list''. Pour défiler d'une ligne vers le bas, on rajoute une ligne au début de la ''display list'' et on enlève la dernière. Et inversement pour défiler vers le haut. Le processeur peut le faire sans se synchroniser fortement avec l'accélérateur de ''scanline''. Nous verrons dans quelques chapitres que ce mécanisme a été utilisé sur les ordinateurs Amstrad PCW, mais nous ne pouvons pas en parler pour le moment, vu que leur carte d'affichage est une carte en mode texte, qu'on n'a pas encore vu.
[[File:Vector display architecture.svg|centre|vignette|upright=1.5|Illustration d'un ''Video Interface Controler''.]]
La ''display list'' fait que les ''raster interrupts'' sont inutiles, ou du moins grandement réduites. Les interruptions sont déclenchées pour certaines lignes seulement, celles où l'instruction qui affiche la ligne le précise. La gestion des interruptions est donc totalement programmable. La ''display list'' contient des instructions pour déclencher des ''raster interrupts'', placées stratégiquement dans la ''display list'' par le programmeur. Par exemple, si le programmeur veut changer les ''sprites'' entre deux lignes, il peut déclencher une ''raster interrupts'' entre ces deux lignes, le processeur répondra à l'interruption en changeant les registres de ''sprites''.
==Le ''Television Interface Adaptor'' des consoles Atari 2600==
Un exemple d'accélérateur de ''scanline'' est celui utilisé sur l'''Atari Video Computer System'' et l'Atari 2600. La carte graphique en question s'appelait la '''''Television Interface Adaptor''''', abrévié TIA. Il incorpore un tampon de ligne, qui est modifié entre l'envoi à l'écran de deux lignes. Par contre, il n'y a pas d'interruption pour prévenir le processeur qu'il doit envoyer la ligne suivante.
===Les contraintes de conception===
Le TIA a été conçu avec de nombreuses contraintes en tête, qui ne se comprennent qu'en remettant la console dans son contexte historique. L'époque est celle des '''consoles de première génération''', des consoles qui étaient conçues pour un jeu bien précis. Pas de cartouche, de CD ou quoique ce soit d'autre. Le jeu était intégré directement dans les circuits de la console, qui n'était pas programmable. Il y avait une console pour le jeu Pong, une autre pour le jeu Tank, etc. Les jeux étaient alors très limités niveau gameplay.
<noinclude>[[File:Pong Game Test2.gif|vignette|upright=2|Pong Game Test2]]</noinclude>
Les premières consoles ATARI étaient les premières '''consoles de seconde génération''', conçues pour faire fonctionner plusieurs jeux, conçues pour être programmables. Elles étaient conçues pour faire fonctionner Pong, Tank et des variantes de ces deux jeux. Pour rappel, s'il faut faire un rappel, Pong était un jeu de tennis à deux joueurs. Il se limitait à un arrière-plan fixe sur lequel chaque joueur contrôle une barre verticale, sur laquelle la balle peut rebondir.
Tank était une variante de PONG, où deux joueurs controlaient chaun un tank et se tiraient des missiles dessus avec l'intention de détruire le tank de l'adversaire. Son adaptation sur ATARI s'appelait Combat!, en voici un screenshot :
[[File:Combat.svg|centre|vignette|upright=2|Combat, jeu de l'Atari 2600.]]
La console est donc conçue pour gérer un arrière-plan, deux sprites pour les joueurs, un pour la balle, deux pour les missiles. Chacun est mémorisé dans un registre dédié, qui mémorise la ligne du sprite/arrière-plan à afficher à l'écran. De plus, le TIA incorpore un système pour détecter les collisions, afin de détecter si la balle rebondit sur la raquette dans PONG, sur un missile touche un tank dans TANK. La conception du TIA est très simple, sans compter que diverses optimisations permettent d'encore plus simplifier le TIA. Par exemple, l'arrière-plan est censé être symétrique : la partie gauche est recopiée sur le côté droit de l'écran, avec un effet miroir. Optimisation possible car PONG et TANK sont censé avoir un terrain de jeu symétrique, identique pour chaque joueur.
Avec une architecture aussi simple, il n'est pas possible de faire de miracles. Les jeux sur ATARI devaient faire avec les limitations du TIA : arrière-plan symétrique, moins d'une dizaine de sprites simultanés à l'écran.
<noinclude>{|
|-
|[[File:Mini Golf Atari 2600.jpg|vignette|Mini Golf, Atari 2600]]
|[[File:Backgammon Atari 2600.jpg|vignette|Backgammon , Atari 2600]]
|[[File:Human Cannonball Atari 2600.jpg|vignette|Human Cannonball, Atari 2600]]
|-
|[[File:Frogs & Flies Atari 2600.jpg|vignette|Frogs & Flies, Atari 2600]]
|[[File:DuckAttack-Tank-room-1.png|vignette|Screenshot de Duck Attack !]]
|[[File:Barnstorming Atari 2600.jpg|vignette|Barnstorming Atari 2600]]
|}</noinclude>
Quelques ruses de sioux ont cependant permis de dépasser ces limites techniques.
{|
|-
|[[File:Spider Fighter Atari 2600.jpg|vignette|Spider Fighter Atari 2600]]
|[[File:Robot Tank Atari 2600.jpg|vignette|Robot Tank Atari 2600]]
|[[File:Bradley Trainer screenshot.png|vignette|Bradley Trainer screenshot]]
|}
===L'arrière-plan du TIA : un registre dédié de 20 bits===
Vu que le rendu s'effectue ligne par ligne, l'arrière-plan et les sprites sont en réalité des lignes, pas des images complètes. Le '''registre d'arrière-plan''' est un registre de 20 bits. Un bit correspond à 4 pixels de l'écran, il indique la couleur de ces 4 pixels consécutifs. Peut-être avez-vous remarquez que 20 bits * 4 pixels = 80 pixels. Pourtant, le TIA gére une résolution horizontale par défaut de 160 pixels. Soit deux fois plus que ce qui est géré par le registre d'arrière-plan. La raison est que l'arrière-plan est symétrique. Le registre d'arrière-plan ne définit que la moitié gauche de l'écran, la moitié droite en est le reflet miroir.
Vous pourriez croire que la couleur codée dans ce registre est un simple noir et blanc, mais c'est plus compliqué. Le registre d'arrière-plan indique, pour chaque bloc de 4 pixels, si le pixel prend la couleur de l'arrière-plan ou la couleur de fond par défaut. Un registre séparé contient la couleur de l'arrière-plan, elle-même encodée grâce à un système de palette indicé. La couleur de fond est elle aussi configurable.
En théorie, l'arrière-plan est censé être symétrique, mais quelques astuces permettent d'afficher un arrière-plan asymétrique. L'idée est simplement de modifier le registre d'arrière-plan une fois que la moitié gauche a été affichée. Cela demande un ''timing'' très précis, mais c'est possible et plusieurs jeux Atari 2600 le faisaient. Certains arrivaient même à simuler un défilement horizontal !
===Les ''sprites'' matériels du TIA===
Le TIA gère 5 ''sprites'' matériels, grâce à 5 registres capables chacun de mémoriser une ligne de pixel. Ces registres sont spécialisés, avec deux registres pour les avatars des joueurs, deux missiles et une balle. Ne vous fiez pas aux noms, la différence entre les 5 est la taille des ''sprites'' stocké dedans. Les ''sprites'' des avatars sont de 8 pixels de large, peuvent être dupliquées ou étendus. La balle est un ''sprite'' de même couleur que l'arrière-plan, de 1, 2, 4 ou 8 pixels de large. Les deux missiles sont identiques à la balle, sauf qu'ils ont la même couleur que le ''sprite'' du joueur associé.
Un ''sprite'' matériel sur le TIA est codé sur un octet. Un ''sprite'' fait 8 pixels de large, chacun étant là-encore codé sur 1 bit. Il indique encore une fois si le pixel a la couleur de fond ou une couleur spécifique au ''sprite''. Vu que cet octet écrit une ligne du ''sprite'', il est changé à chaque ligne pour réellement afficher un ''sprite'' à deux dimensions. Vous remarquerez que le ''sprite'' est codé sur un octet, les informations de sa position X et Y à l'écran ne sont pas encodées. La raison est simple : un ''sprite'' est affiché quand le processeur écrit l'octet dedans.
L'affichage d'un ''sprite'' est donc commandé par le processeur et doit se faire au cycle d'horloge près. Le problème est que le processeur n'est pas précis au pixel près. Déjà, sa fréquence est égale à un tiers de la fréquence du TIA, ce qui fait qu'il est précis à 3 pixels près. De plus, le logiciel faisait l'écriture dans le registre de ''sprite'' est basé sur une boucle, dont les instructions prennent 5 cycles pour s'exécuter. Ce qui fait une imprécision de 15 cycles.
Pour compenser ce défaut, le TIA incorpore deux registres, un par avatar/''player''. Ils permettent de décaler le ''sprite'' de quelques pixels, la valeur de décalage étant précisée dans ce registre. La valeur est un entier allant de -7 à 8 cycles/pixels. L'affichage d'un ''sprite'' est donc en deux temps : on déclenche une écriture précise à 15 cycles près, puis on décale le ''sprite'' avec ces deux registres.
===L'architecture du TIA===
Mettre à jour les registres du TIA à chaque ligne n'est pas si différent de modifier une mémoire vidéo entre deux lignes. Mais un défaut des consoles ATARI est que le processeur n'utilisait pas d'interruptions pour gérer l'affichage des lignes. À la place, le bus indiquait au processeur quand le TIA acceptait qu'on modifie ses registres. Le bus avait un signal RDY (READY) qui indiquait si le TIA était occupé à afficher une ligne ou non.
Un autre défaut est que le TIA ne génère pas totalement les signaux de synchronisation verticale, qui servent à indiquer à l'écran qu'il a fini d'afficher une image. La raison est qu'il n'a pas de compteur de ligne ! Il y a bien un compteur pour les colonnes, pour générer les signaux de synchronisation horizontale, mais pas plus. C'est le processeur qui doit générer le signal de synchronisation verticale de lui-même ! Pour cela, le processeur doit écrire dans une adresse dédie associée au TIA, ce qui déclenche un signal de synchronisation verticale immédiat.
Si on devait faire un constat avec tout ce qui a été dit plus haut, il serait que le TIA est très simple, trop simple. Le processeur est très impliqué dans l'affichage à l'écran, notamment pour les ''sprites''. La carte graphique est donc composé par l'association entre le TIA et le processeur proprement dit, bien que celui-ci soit le CPU. Ce qui explique l'absence de mécanismes de synchronisation entre TIA et CPU : c'est le CPU qui commande le TIA au cycle près.
==Les VDC ANTIC et CTIA des consoles Atari post-2600==
Les consoles Atari 8 bits, qui ont succédé à l'Atari 2600, n'utilisaient un VDC TIA amélioré appelé le CTIA pour sa première version (''Color Television Interface Adaptor''), le GTIA pour sa seconde version (''Graphic Television Interface Adaptor''). Les deux étaient un accélérateur de ''scanline'' qui intégraient l'accélération matérielle de ''sprites'' et une palette indicée. Comme pour le TIA, les ''sprites'' servaient pour les joueurs et des missiles, sauf que leur nombre est doublé pour passer à 4 chacun.
Mais la révolution était qu'ils étaient associé à un autre circuit appelé l'ANTIC, qui commandait le CTIA/GTIA sans intervention du processeur CPU. Là où l'Atari 2600 utilisait le processeur pour l'affichage des ''sprites'' et beaucoup d'autres fonctions, une bonne partie de ces traitements est déléguées à l'ANTIC sur les machines suivantes. L'ANTIC est un ''blitter'' amélioré, en charge des copies en mémoire. Mais surtout, il s'occupe du séquencement des lignes envoyées au CTIA. Ce n'est pas le processeur qui s'occupe d'envoyer les lignes une par une au CTIA, cette fonction est déportée sur l'ANTIC.
===La ''display list''===
Le fonctionnement de l'ANTIC est assez complexe, mais on peut le voir comme une sorte de pseudo-processeur, qui exécute un programme de rendu 2D. Le programme en question est une suite d’opération de rendu 2d à exécuter dans l'ordre, qui permet de calculer l'image ligne par ligne. Il est appelé la '''''Display List''''', et est stocké dans la mémoire de la console. L'ANTIC lit le programme instruction par instruction, grâce à un contrôleur DMA intégré dans l'ANTIC, qui contient un ''program counter'' interne à l'ANTIC. L'exécution d'une ''Display List'' sur l'ANTIC permet de décharger le processeur des tâches de rendu 2D, du moins dans une certaine mesure.
Une instruction de la ''display list'' permet de calculer une ligne de l'écran, en précisant comment combiner les ''sprites'' et l'arrière-plan, tous deux stockés dans la mémoire vidéo. La ''display list'' gére entre 0 et 240 instructions, ce qui limite la résolution verticale à 240 pixels. Les instructions peuvent se classer en quatre types principaux : des instructions pour écrire une ligne de pixels colorés, des instructions pour écrire une ligne de caractères, des instructions pour émettre les signaux de synchronisation de ''blanking'', et enfin des branchements qui agissent sur le ''program counter'' de l'ANTIC. Les instructions pour afficher des pixels/caractères peuvent activer le défilement horizontal ou vertical suivant l'instruction.
Une instruction est codée sur 8 bits minimum.
{| class="wikitable"
|-
! Bit 7 !! Bit 6 !! Bit 5 !! Bit 4 !! Bit 3 !! Bit 2 !! Bit 1 !! Bit 0
|-
| DLI || LMS || VS || HS || colspan="4" | Mode
|}
Les 4 bits de mode précisent quelle est l'instruction à utiliser, ce qui fait 16 instructions en tout, réparties dans les 4 classes mentionnées plus haut. Les bits HS et VS activent ou désactivent le défilement vertical et horizontal. Le bit LMS permet à l'instruction de préciser l'adresse où se trouvent les tableaux de données à utiliser, comme la place de l'arrière-plan en mémoire RAM, ce genre de choses. Pour cela, le bit est placé à 1 et l'adresse est dans les 2 octets qui suivent l'instruction. Le bit DLI gère les interruptions dites de ''Display List'', ou interruptions DLI, que nous allons voir dans ce qui suit.
Les interruptions DLI sont l'équivalent sur l'ANTIC des interruptions d'''horizontal blanking'', déclenchées à la fin de l'affichage d'une ligne. Elles préviennent le processeur qu'une ligne entière a été affichée. Le processeur peut alors écrire en mémoire vidéo durant un court laps de temps s'il en a besoin. Il peut alors changer les ''sprites'' à la volée, ce qui permet d'afficher plus de ''sprites'' que ce que supporte le chip CTIA/GTIA. Les interruptions sont déclenchées pour certaines lignes seulement, celles où l'instruction qui affiche la ligne le précise. Si l'instruction a son bit DLI mis à 1, alors une interruption est déclenchée à la fin de la ligne. S'il est à 0, pas d'interruption. La gestion des interruptions DLI est donc totalement programmable.
===Les registres de configuration de l'ANTIC===
[[File:Atari ANTIC (CO12296) pin-out.svg|vignette|Atari ANTIC (CO12296) pin-out]]
Niveau registres, l'ANTIC contient un ''program counter'' et de quoi configurer le contrôleur DMA. Il a aussi des registres pour localiser les données importantes en mémoire, qui sont regroupées dans des tableaux séparés.
Pour ce qui est du rendu, il contient un registre pour l'adresse de l'arrière-plan, un autre pour l'adresse des ''sprites'' en mémoire RAM. Il contient aussi deux registres pour configurer le défilement horizontal et vertical.
Pour gérer la synchronisation avec le CPU, l'ANTIC incorpore un compteur de ligne, ainsi qu'un registre de synchronisation horizontale qui est à 1 quand une ligne est en train d'être affichée (mécanisme redondant avec les interruptions DLI). Il y a aussi plusieurs registres pour gérer les interruptions, avec un registre de statut que le CPU peut lire à loisir pour savoir quelle est la raison qui a déclenché l'interruption, ainsi qu'un registre de configuration des interruptions.
==Le système graphique de la console néo-géo==
La néo-géo est une console assez ancienne, de la période 16 bits, commercialisée en 1990. Elle avait pour caractéristique d'avoir le même hardware que les bornes d'arcade du même nom, les néo-géo MVS. Elle comprenait un processeur Motorola 68000, 64 kilooctets de mémoire RAM, un processeur sonore Zilog Z80, et un VDC avec 64 kilooctets de mémoire vidéo (plus 4 kilooctets de VRAM complémentaire). Elle avait une palette de 65 536 couleurs, grâce à une palette indicée de 8 kilooctets.
===L'avant-plan, l'arrière-plan et les ''sprites'' de la néo-géo===
Un point important est que la mémoire vidéo n'est pas un ''framebuffer'', le VDC utilise un tampon de ligne. La console a pour particularité de ne pas gérer d’arrière-plan, l'image affichée est intégralement composée de ''sprites''. Les ''sprites'' sont tous combinés pour donner l'image finale.
Un autre détail est que les ''sprites'' matériels sur la néo-géo sont différents des ''sprites'' matériels des consoles précédentes. Ils sont composés de plusieurs motifs, de plusieurs ''tiles''. Les motifs font tous des carrés de 16 pixels de côtés, contenant 15 couleurs (plus la couleur de transparence). Un ''sprite'' est un rectangle qui a un pixel de large, mais dont la hauteur va de 1 à 32 motifs, donc de 16 à 512 pixels. Chaque ''sprite'' indique quels sont ses motifs, quelle palette utiliser, s'il faut appliquer l'effet miroir, etc. Un ''sprite'' logiciel est composé en concaténant horizontalement plusieurs ''sprites'' de néo-géo.
A défaut d'un arrière-plan, la console gère un pseudo-arrière-plan composé d'une couleur unie. Pour faire la différence avec une image d’arrière-plan, on peut plutôt parler d'une couleur de fond. Les ''sprites'' sont empilés les uns sur les autres au-dessus de cet arrière-plan. Au-dessus des ''sprites'', il y a un avant-plan spécialisé, qui est rendu au-dessus de tout le reste, qui est utilisé pour rendre le HUD. Cet avant-plan est appelé la ''fix layer'' dans la documentation de la console. Les motifs de l'avant-plan n'ont pas la même taille que pour les ''sprites'' : ce sont des carrés de 8 pixels de côtés, pas 16 !
Les motifs de l'avant-plan sont placés dans une mémoire ROM de celle pour les motifs des ''sprites''. En effet, une cartouche de néo-géo contient plusieurs mémoires ROM :
* une pour les motifs des ''sprites'' ;
* une pour les motifs de l'avant-plan ;
* une pour le programme à exécuter sur le processeur M68000 principal ;
* une pour le programme à exécuter sur le processeur sonore Z80 ;
* une pour les échantillons audio, la musique et le son du jeu.
===Le rendu d'une image===
Lors du rendu d'une ligne, la console détermine quels ''sprites'' sont affichés sur cette ligne, et les ''sprites'' adéquats sont lus depuis la ROM de la cartouche. Les pixels de la ligne sont déterminés par le VDC et écrit dans le tampon de ligne. La console supporte au maximum 96 ''sprites'' par ligne, ce qui dépasse de très loin les capacités des autres consoles 8 ou 16 bits.
La console dispose de deux tampons de lignes, ce qui permet d'afficher une ligne pendant qu'on prépare la suivante. L'idée est la suivante. La première ligne est placée dans un tampon de ligne, elle est envoyée à l'écran pixel par pixel par le VDC. En même temps, le processeur écrit la prochaine dans le second tampon de ligne. Une fois la ligne totalement affichée, le VDC échange les deux tampons de ligne. La ligne suivante est déjà prête dans le second tampon de ligne, elle est envoyée à l'écran pixel par pixel, le premier tampon de ligne est utilisé pour précalculer la ligne suivante, et ainsi de suite.
La gestion de la profondeur des ''sprites'' est assez simple : les ''sprites'' sont placés dans la mémoire vidéo selon leur profondeur. En clair, les ''sprites'' situés au fond de l'image sont placés au début de la mémoire vidéo, ceux situés à l'avant-plan sont placés à la fin de la mémoire vidéo. Pas besoin po_ur le VDC de gérer les priorités et la profondeur des ''sprites'', tout est déjà réglé une fois les ''sprites'' chargés dans la mémoire vidéo.
Le console gère une mise à l'échelle des ''sprites'' matériels, mais ceux-ci ne peuvent qu'être réduits, pas zoomés. La mise à l'échelle est précise au pixel près. La mise à l'échelle peut se faire à l'horizontale et à la verticale, les deux sont séparés. Par exemple, on peut réduire un ''sprite'' d'un facteur 5 sur la verticale, d'un facteur 2 sur l'horizontale.
{{NavChapitre | book=Les cartes graphiques
| prev=Le mode texte et le rendu en tiles
| prevText=Le mode texte et le rendu en tiles
| next=Les Video Display Controler atypiques
| nextText=Les Video Display Controler atypiques
}}
{{autocat}}
1aw2hw37ss2os7wkgu3dib4pmi06wr6
Fonctionnement d'un ordinateur/Le contournement (data forwarding)
0
82832
774563
765279
2026-10-09T21:02:00Z
Mewtow
31375
774563
wikitext
text/x-wiki
Les dépendances RAW correspondent au cas où une instruction a pour opérande le résultat d'une instruction précédente. Vous vous dites que le résultat d'une opération doit être enregistré dans les registres avant de pouvoir être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Pour gérer les dépendances RAW, on a deux grandes solutions. La première solution est de retarder l'exécution de la seconde instruction tant que la première n'a pas enregistré son résultat dans les registres, en bloquant le pipeline. La technique du '''contournement''' (''bypass'') rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. Le présent chapitre est dédié uniquement à cette technique dite de contournement.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
==Le contournement sur les pipelines 1-cycle==
Dans cette section, on étudie un pipeline 1-cycle, ce qui veut dire que toutes les instructions s'exécutent en un seul cycle d'horloge. De plus, il n'y a qu'une seule unité de calcul, combinée avec une unité d'accès mémoire. La raison est que les instructions multicycles compliquent la mise en place du contournement. Il en est de même avec la présence de plusieurs unités de calcul.
===Le contournement de l'unité de calcul===
La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée.
[[File:Data Forwarding (One Stage).svg|centre|vignette|upright=1.5|Principe du contournement avec le pipeline RISC classique.]]
Pour cela, on connecte la sortie de l'unité de calcul sur son entrée si besoin, et on la déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs.
[[File:Bypassing avec des multiplexeurs.png|centre|vignette|upright=2|Contournement avec des multiplexeurs.]]
Pour détecter les dépendances, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul.
[[File:Implémentation complète du contournement.png|centre|vignette|upright=2|Implémentation complète du contournement]]
===Le contournement des lectures mémoire===
La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand unelecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type ''load-op'', qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué).
Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre, mais cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire.
[[File:Implémentation du contournement de l'unité mémoire.png|centre|vignette|upright=2|Implémentation du contournement de l'unité mémoire]]
La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le '''l'unité de contournement'''. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs.
[[File:Implémentation complète du contournement mémoire avec un multiplexeur.png|centre|vignette|upright=2|Implémentation complète du contournement mémoire avec un multiplexeur]]
Mais ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt.
[[File:Data Forwarding (Two Stage, error).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage, error)]]
La solution est de retarder le résultat d'un cycle d'horloge en bloquant le pipeline. En clair, on économise un cycle au lieu de deux.
[[File:Data Forwarding (Two Stage).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage)]]
La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour.
===Le contournement des écritures mémoire===
Après avoir vu le contournement pour les lectures, voyons maintenant le contournement des écritures. Au premier abord, cela peut paraitre paradoxal. Une écriture ne fournit pas de résultat, il n'y a rien à contourner. Cependant, il faut prendre en compte le fait que le processeur gère des exceptions précises. Pour rappel, les exceptions précises imposent que les écritures se fassent à la toute fin du pipeline, dans l'étage de ''Writeback''. Pour cela, les écritures sont sont mises en attente jusqu'à l'étage de ''Writeback''.
Pour rappel, la mise en attente se fait grâce à une mémoire spécialisée, appelée la file d'écriture. C'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre normal des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]]
Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Le cas est rare, car il demande de lire une donnée qu'on vient d'écrire. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Il s'agit bien d'une dépendance RAW assez particulière. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide.
Il est possible d'éviter cela à l'émission, bloquant le pipeline, mais cela demande de comparer des adresses et c'est clairement compliqué. Aussi, une autre solution utilise une forme de contournement pour résoudre ce problème. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''.
[[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]]
==Le contournement sur les pipelines multi-cycle==
L'usage d'instructions multicycles complexifie grandement d'implémentation du contournement. Et il y a plusieurs raisons à cela. La première est la présence de plusieurs unités de calcul, qui complexifie les interconnexions. La seconde est que la durée des instructions est à prendre en compte pour effectuer un contournement convenable.
===La durée des instructions est à prendre en compte===
Les pipelines multicycles peuvent, en théorie, éviter de bloquer le pipeline grâce au contournement. Ce n'est plus le cas en présence d'instructions multicycles. Un exemple parlant est celui où une multiplication de 5 cycles est suivie par une addition d'un cycle. L'addition a pour opérande le résultat de la multiplication. Les deux instructions ne peuvent pas être émises l'une après l'autre sans bloquer le pipeline. Le résultat de la multiplication est disponible après 5 cycles, l'addition doit attendre 4 cycles avant d'être émises, sans quoi elle entrera dans l'ALU trop tôt, avant que le multiplieur fournisse le résultat voulu.
L'unité d'émission doit retarder l'émission d'une instruction tant que le résultat n'est pas disponible, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas.
===La présence de plusieurs ALU et le réseau de contournement===
Le premier problème est la présence de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque ALU sur l'entrée de tous les autres. Le nombre d'interconnexion est alors assez important. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2 * N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul. A la place, les interconnexions sont alors simplifiées, au prix d'une petite perte de performance.
L'ensemble des interconnexions pour le contournement s'appelle le '''réseau de contournement'''. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires.
Il est fréquent que les unités de calcul d'adresse utilisent des opérandes calculées par les ALU entières. En conséquence, il faut relier les sorties des ALU entières aux unités de calcul d'adresse. Cela fait des interconnexions nécessaires, qui ont un impact important pour les performances. Mais les connexions inverses, de l'ALU de calcul d'adresse vers l'ALU entière, ne servent pas à grand chose. La connexion ALU-AGU se fait dans un sens seulement.
: Dans le même genre, la sortie de l'unité mémoire est reliée aux entrées de toutes les autres unités de calcul, pas le choix. Mais cela dépend un peu du processeur.
Le contournement doit prendre en compte la présence d'une FPU si elle existe. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un avec la ou les FPUs. Il arrive que le processeur ait plusieurs FPUS et c'est la même chose, le réseau de contournement entre FPUs est juste plus complexe.
[[File:Réseau de contournement avec plusieurs ALU.png|centre|vignette|upright=2|Réseau de contournement avec plusieurs ALU]]
==Le contournement avec remise en ordre des écritures==
Nous avons vu il y a quelques chapitres que les pipelines dynamiques on un problème d'inversion des écritures. Deux instructions peuvent s'exécuter l'une après l'autre et enregistrer leurs résultats dans l'ordre inverse. Mais les processeurs incorporent souvent des techniques pour remettre les écritures dans l'ordre, qu'il s'agisse de registres d'échelonnement, d'un tampon de ré-ordonnancement, etc. L'idée est que les résultats à écrire ne sont pas enregistrés dans les registres directement, mais sont mis en attente dans une mémoire temporaire, pour être enregistrés dans les registres seulement quand les conditions sont réunies. Nous ferons quelques rappels sur cette mémoire dans ce qui suit.
La présence de cette mémoire temporaire perturbe un peu le contournement. Elle n'impacte pas les interconnexions entre unités de calcul. Mais le contournement devrait en théorie lire les opérandes depuis cette mémoire temporaire. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi.
===Le contournement avec les registres d'échelonnage===
La première technique que nous allons voir est celle des registres d'échelonnage. Pour rappel, l'idée est que toutes les instructions prennent le même nombre de cycles d'horloge. Par exemple, toutes les opérations doivent faire 5 cycles. Si l'addition fournit son résultat en un cycle, il sera enregistré dans les registres avec un retard de quatre cycles. Le retard dépend de l’opération à effectuer, en fonction de combien de cycles elle prend dans l'ALU. En clair, toutes les instructions ont le même nombre d'étages, mais certains étages sont inutiles pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire.
Reste à ajouter des cycles de retard, qui servent juste à retarder l'enregistrement dans les registres. La solution la plus simple insére des ''staggering registers'', des '''registres d'échelonnage''', en sortie des ALU. Les registres d'échelonnage peuvent être regroupés dans un banc de registre unique.
[[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Regsitres d'échelonnage]]
[[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]]
Pour les pipelines de longueur fixe, le contournement ne doit pas uniquement relier la sortie de l'ALU, mais doit aussi tenir compte des registres d'échelonnage. En effet, imaginez qu'une instruction ait pour opérande le résultat d'une instruction exécutée deux cycles plus tôt. Il y a un délai de deux cycles entre les deux, mais l'ALU a fournit un résultat en un cycle d'horloge. Où est le résultat voulu ? Il n'est pas en sortie de l'ALU, mais dans le registre d'échelonnage juste après. Le contournement doit donc relier les registres d'échelonnage aux entrées des unités de calcul.
Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Mais faire ainsi nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et bloque le pipeline si besoin.
===Le contournement avec un tampon de ré-ordonnancement===
Les processeurs modernes ajoutent un tampon de ré-ordonnancement, pour remettre les écritures dans les registres dans l'ordre du programme. Les données à écrire sont mise en attente dans le tampon de ré-ordonnancement, qui est une sorte d'hybride entre mémoire cache et mémoire FIFO.
[[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]]
Les données stockées dans le ROB sont certes mises en attente, mais qu'il est possible que certains calculs en aient besoin. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Mais pour faire ainsi, il faut que le processeur implémente des techniques dites de renommage de registre que nous verrons dans quelques chapitres.
Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail.
Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes.
==L'émission anticipée des micro-opérations==
Idéalement, on voudrait démarrer une nouvelle instruction sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU.
Si une instruction réutilise le résultat d'une autre, cela peut poser problème. Imaginons que l'on ait trois cycles entre la file d'attente et les ALU. Si une instruction est émise quand l'opérande sorte de l'ALU, elle prendrait plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Et pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d''''émission anticipée'''.
Le nombre de cycles d'avance dépend de l'opération, précisément du temps d'exécution de l'opération dans l'ALU. Le cas le plus simple est celui d'une opération qui s’exécute en un seul cycle d'horloge, comme une addition ou une soustraction. Le nombre de cycles d'avance est alors le nombre d'étages entre l'unité de calcul et la fenêtre d'instruction. Pour une instruction multicycle, il faut ajouter le nombre de cycles que l'opération passe dans l'ALU (moins un cycle). La conséquence est que cela ne marche que pour des instructions qui ont une durée fixe, connue à l'avance. Et autant c'est le cas pour les instructions arithmétiques et logiques, autant ce n'est pas le cas pour les accès mémoire.
===L'émission anticipée des accès mémoire===
Plus haut, nous avons vu comment appliquer le contournement à l'unité mémoire. Mais cela ne marche que dans un cas simple, où une lecture fait un cycle d'horloge. Dans les processeurs modernes, les accès mémoire ont une latence variable : quelques cycles pour un succès de cache, plusieurs dizaines de cycles pour un défaut de cache L1, pas loin de la centaine pour un défaut de cache L2, etc. Et cela pose des problèmes pour l'émission anticipée et le contournement en général.
L'émission en avance pour les accès mémoire est purement spéculative : le processeur émet des instructions en supposant que la lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont alors émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Et pour corriger la situation, il y a plusieurs solutions.
Le pipeline RISC classique gèle totalement le processeur en cas de défaut de cache. Les instructions restaient à leur place, elles ne bougeaient pas, et le processeur reprenait là où il en était une fois le défaut de cache terminé. Mais cette solution, bien qu'idéale, n'est pas la plus pratique. Une solution alternative ne gèle que l'instruction dépendante sur place, dans le pipeline. L'instruction dépendante attend en entrée de l'unité de calcul que l'opérande nécessaire soit disponible, que le défaut de cache se passe. Le problème est que tout ce qui précède l'unité de calcul doit aussi être gelé : l'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Le défaut de cette solution est qu'elle est difficile à mettre en œuvre, sauf à geler totalement le processeur comme le faisaient les processeurs avec un pipeline RISC classique, comme dit plus haut.
Une autre solution annule toutes les instructions après celles émise à tort, avec les circuits pour les exceptions précises. Un défaut de cache déclenche une exception matérielle interne au processeur. Le processeur élimine alors les toutes instructions émises après le défaut de cache, dont les instructions lecture-dépendantes mais pas que. Une autre solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre.
Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités.
===Le ''replay pipeline'' du Pentium 4===
Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande.
L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ».
[[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]]
Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire.
Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''.
: La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance.
[[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]]
Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente.
Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple).
[[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]]
Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps.
[[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]]
On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'émission dans l'ordre des instructions
| prevText=L'émission dans l'ordre des instructions
| next=Les premiers processeurs Intel
| nextText=Les premiers processeurs Intel
}}{{autocat}}
</noinclude>
skot9815qz6o8ggqx69dyui4wl78zpg
774564
774563
2026-10-09T21:02:53Z
Mewtow
31375
774564
wikitext
text/x-wiki
Les dépendances RAW correspondent au cas où une instruction a pour opérande le résultat d'une instruction précédente. Vous vous dites que le résultat d'une opération doit être enregistré dans les registres avant de pouvoir être utilisé comme opérande. Et cette écriture a lieu à la fin du pipeline, ce qui fait qu'il y a quelques cycles entre le moment où le résultat est calculé et enregistré. Pour gérer les dépendances RAW, on a deux grandes solutions. La première solution est de retarder l'exécution de la seconde instruction tant que la première n'a pas enregistré son résultat dans les registres, en bloquant le pipeline. La technique du '''contournement''' (''bypass'') rend le résultat utilisable directement dans le pipeline, avant d'être enregistré dans les registres. Le présent chapitre est dédié uniquement à cette technique dite de contournement.
[[File:Pipeline Bypass.png|centre|vignette|upright=1|Pipeline Bypass]]
==Le contournement de l'unité de calcul==
La technique du contournement la plus simple implique uniquement l'unité de calcul. Elle permet à un résultat en sortie de l'unité de calcul d'être réutilisé immédiatement en entrée.
[[File:Data Forwarding (One Stage).svg|centre|vignette|upright=1.5|Principe du contournement avec le pipeline RISC classique.]]
Pour cela, on connecte la sortie de l'unité de calcul sur son entrée si besoin, et on la déconnecte sinon. La connexion/déconnexion se fait avec des multiplexeurs.
[[File:Bypassing avec des multiplexeurs.png|centre|vignette|upright=2|Contournement avec des multiplexeurs.]]
Pour détecter les dépendances, il faut comparer le registre destination avec le registre source en entrée : si ce registre est identique, on devra faire commuter le multiplexeur pour relier la sortie de l'unité de calcul.
[[File:Implémentation complète du contournement.png|centre|vignette|upright=2|Implémentation complète du contournement]]
==Le contournement des lectures mémoire==
La technique précédente gère les dépendances RAW pour les instructions de calcul uniquement. En clair, elle gére le cas où le résultat d'une instruction de calcul est utilisé comme opérande par une instruction suivante. Mais on peut gérer le cas où l'opérande est lu en mémoire. Une telle situation survient quand unelecture lit une donnée qui sert d'opérande à une instruction arithmétique. Elle survient aussi sur les architectures CISC, pour gérer les instructions de type ''load-op'', qui fusionnent une lecture et une opération en une seule instruction (le pipeline de tels processeurs est assez compliqué).
Une lecture lit une donnée, qui est utilisée comme opérande par d'autres instructions, généralement des instructions de calcul, parfois d'autres lectures : appelons-les les '''instructions lecture-dépendantes''', sous-entendu dépendantes du résultat de la lecture. Le contournement permet d'utiliser le résultat de la lecture avant qu'il soit enregistré dans le banc de registre, mais cela demande de fortement modifier le pipeline. Précisément, il faut rajouter une interconnexion qui part de l'unité d'accès mémoire et se connecte sur l'entrée de l'ALU. Le tout est très similaire au contournement de l'unité de calcul et demande juste d'ajouter une entrée sur le multiplexeur, sur laquelle on relie la sortie de l'unité de lecture/mémoire.
[[File:Implémentation du contournement de l'unité mémoire.png|centre|vignette|upright=2|Implémentation du contournement de l'unité mémoire]]
La seule complexité est de commander le multiplexeur. Pour cela, on rajoute un second comparateur, qui compare le registre opérande avec le registre de destination fournit par l'unité mémoire. Il y a donc au total deux comparateurs, dont les résultats sont combinés pour commander le multiplexeur. Les comparateurs et le circuit de combinaison sont souvent regroupés dans un seul circuit appelé le '''l'unité de contournement'''. D'autres processeurs ont des unités de contournement plus élaborée, mais dont le principe est le même : comparer les registres destination et opérande, en déduire de quoi configurer les multiplexeurs.
[[File:Implémentation complète du contournement mémoire avec un multiplexeur.png|centre|vignette|upright=2|Implémentation complète du contournement mémoire avec un multiplexeur]]
Mais ce qui vient d'être dit marche uniquement si on a une unité mémoire qui fonctionne en parallèle de l'unité de calcul. Sur le pipeline RISC classique, l'unité de calcul et l'unité mémoire sont placées en série, et les choses sont plus compliquées. En effet, il y a deux cycles de différence entre l'entrée de l'unité de calcul et la sortie de l'unité mémoire. La conséquence est que la donnée lue est disponible deux cycles après l'entrée dans l'ALU, alors que l'instruction de calcul démarre un cycle après l'instruction d'accès mémoire. Il y a un cycle de différence qui fait que le résultat est connu un cycle trop tôt.
[[File:Data Forwarding (Two Stage, error).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage, error)]]
La solution est de retarder le résultat d'un cycle d'horloge en bloquant le pipeline. En clair, on économise un cycle au lieu de deux.
[[File:Data Forwarding (Two Stage).svg|centre|vignette|upright=1.5|Data Forwarding (Two Stage)]]
La commande du multiplexeur devient aussi plus compliquée. On retrouve les comparateurs entre registres destination et source, mais le délai fait que des subtilités se font jour.
==Le contournement des écritures mémoire==
Après avoir vu le contournement pour les lectures, voyons maintenant le contournement des écritures. Au premier abord, cela peut paraitre paradoxal. Une écriture ne fournit pas de résultat, il n'y a rien à contourner. Cependant, il faut prendre en compte le fait que le processeur gère des exceptions précises. Pour rappel, les exceptions précises imposent que les écritures se fassent à la toute fin du pipeline, dans l'étage de ''Writeback''. Pour cela, les écritures sont sont mises en attente jusqu'à l'étage de ''Writeback''.
Pour rappel, la mise en attente se fait grâce à une mémoire spécialisée, appelée la file d'écriture. C'est une mémoire FIFO, le caractère FIFO permettant de conserver l'ordre normal des écritures. L'idée est la suivante : l'étage MEM fait l'écriture dans la file d'écriture, puis l'étage de ''Writeback'' transfère la donnée de la file d'écriture vers le cache. L'écriture est mise en attente dans l'étage MEM, et est finalisée dans l'étage de ''Writeback''. Si une exception est détectée, l'étage de ''Writeback'' vide la file d'écriture, ce qui annule les écritures mises en attente. Les écritures faites à tord sont donc annulées.
[[File:Store Queue sur un pipeline fixe.png|centre|vignette|upright=2|File d'écriture sur un pipeline fixe]]
Les écritures sont donc retardées de plusieurs cycles, pour s'exécuter en fin de pipeline. Et il se peut que la donnée écrite soit lue pendant ce laps de temps. Le cas est rare, car il demande de lire une donnée qu'on vient d'écrire. Il s'agit du cas où une instruction STORE est suivie par une instruction LOAD, et que les deux accèdent à la même adresse. Il s'agit bien d'une dépendance RAW assez particulière. Vu que la lecture a lieu pendant la mise en attente, la donnée n'a pas été écrite dans le cache. Toute lecture dans le cache lira une donnée invalide.
Il est possible d'éviter cela à l'émission, bloquant le pipeline, mais cela demande de comparer des adresses et c'est clairement compliqué. Aussi, une autre solution utilise une forme de contournement pour résoudre ce problème. L'idée est que les lectures consultent à la fois le cache et la file d'écriture. La file d'écriture doit cependant être modifiée pour fonctionner comme un mélange entre mémoire cache et mémoire FIFO. Le cache envoie l'adresse à lire à la file d'écriture, et celle-ci vérifie si une écriture est en attente à cette adresse. Si c'est le cas, c'est un ''succès de file d'écriture'', et celle-ci envoie alors la donnée associée à l'étage d'accès mémoire. Sinon, la lecture accède au cache, là où se trouve la donnée à lire. La solution porte le nom de ''Store to load Forwarding'' ou encore de '''réacheminement écriture-vers-lecture'''.
[[File:Store to load forwarding sur un pipeline simple.png|centre|vignette|upright=2|Store to load forwarding sur un pipeline simple]]
==Le contournement sur les pipelines multi-cycle==
L'usage d'instructions multicycles complexifie grandement d'implémentation du contournement. Et il y a plusieurs raisons à cela. La première est la présence de plusieurs unités de calcul, qui complexifie les interconnexions. La seconde est que la durée des instructions est à prendre en compte pour effectuer un contournement convenable.
Les pipelines multicycles peuvent, en théorie, éviter de bloquer le pipeline grâce au contournement. Ce n'est plus le cas en présence d'instructions multicycles. Un exemple parlant est celui où une multiplication de 5 cycles est suivie par une addition d'un cycle. L'addition a pour opérande le résultat de la multiplication. Les deux instructions ne peuvent pas être émises l'une après l'autre sans bloquer le pipeline. Le résultat de la multiplication est disponible après 5 cycles, l'addition doit attendre 4 cycles avant d'être émises, sans quoi elle entrera dans l'ALU trop tôt, avant que le multiplieur fournisse le résultat voulu.
L'unité d'émission doit retarder l'émission d'une instruction tant que le résultat n'est pas disponible, et ce n'est pas une mince affaire. Par exemple, dans l'exemple précédent, l'unité d'émission doit savoir quand le résultat de la multiplication est disponible. L'exemple précédent était simple, avec deux instructions consécutives. Maintenant, imaginez que la multiplication soit suivie par une addition, elle-même suivie d'une soustraction, et que c'est la soustraction qui utilise le résultat de la multiplication. Dans ce cas, la soustraction arrive dans l'unité d'émission deux cycles après la multiplication, elle doit être retardée de 3 cycles et non de 4. L'unité d'émission doit gérer correctement ce genre de cas.
Le premier problème est la présence de plusieurs unités de calcul. Intuitivement, on se dit qu'il faut envoyer la sortie de chaque ALU sur l'entrée de tous les autres. Le nombre d'interconnexion est alors assez important. Pour N unités de calcul, cela demande 2 * N² interconnexions, implémentées avec 2 * N multiplexeurs de N entrées chacun. Si c'est faisable pour 2 ou 3 ALUs, la solution est impraticable sur les processeurs modernes, qui ont facilement une dizaine d'unité de calcul. A la place, les interconnexions sont alors simplifiées, au prix d'une petite perte de performance.
L'ensemble des interconnexions pour le contournement s'appelle le '''réseau de contournement'''. Il peut être très complexe, proche d'une interconnexion totale (toutes les sorties sur toutes les entrées) ou au contraire réduit au minimum. Concevoir un réseau de contournement demande de vérifier quelles interconnexions possibles sont vraiment utiles. Il est des interconnexions qui sont inutiles, d'autres qui sont utiles dans des cas assez rares, d'autres qui ne sont tout simplement pas nécessaires.
Il est fréquent que les unités de calcul d'adresse utilisent des opérandes calculées par les ALU entières. En conséquence, il faut relier les sorties des ALU entières aux unités de calcul d'adresse. Cela fait des interconnexions nécessaires, qui ont un impact important pour les performances. Mais les connexions inverses, de l'ALU de calcul d'adresse vers l'ALU entière, ne servent pas à grand chose. La connexion ALU-AGU se fait dans un sens seulement.
: Dans le même genre, la sortie de l'unité mémoire est reliée aux entrées de toutes les autres unités de calcul, pas le choix. Mais cela dépend un peu du processeur.
Le contournement doit prendre en compte la présence d'une FPU si elle existe. En théorie, on devrait envoyer les résultats flottants à l'ALU entière et les résultats entiers à la FPU. Mais dans les faits, cela ne servirait pas à grand chose. Il est rare que les instructions entières et flottantes échangent des données. Et si elles le font, elles peuvent passer par des copies registres, des registres flottants vers entiers et inversement. Aussi, le contournement entre ALU entière et FPU n'est presque jamais implémenté. Le processeur utilise alors deux réseaux de contournement : un entre les ALUs entières, un avec la ou les FPUs. Il arrive que le processeur ait plusieurs FPUS et c'est la même chose, le réseau de contournement entre FPUs est juste plus complexe.
[[File:Réseau de contournement avec plusieurs ALU.png|centre|vignette|upright=2|Réseau de contournement avec plusieurs ALU]]
==Le contournement avec remise en ordre des écritures==
Nous avons vu il y a quelques chapitres que les pipelines dynamiques on un problème d'inversion des écritures. Deux instructions peuvent s'exécuter l'une après l'autre et enregistrer leurs résultats dans l'ordre inverse. Mais les processeurs incorporent souvent des techniques pour remettre les écritures dans l'ordre, qu'il s'agisse de registres d'échelonnement, d'un tampon de ré-ordonnancement, etc. L'idée est que les résultats à écrire ne sont pas enregistrés dans les registres directement, mais sont mis en attente dans une mémoire temporaire, pour être enregistrés dans les registres seulement quand les conditions sont réunies. Nous ferons quelques rappels sur cette mémoire dans ce qui suit.
La présence de cette mémoire temporaire perturbe un peu le contournement. Elle n'impacte pas les interconnexions entre unités de calcul. Mais le contournement devrait en théorie lire les opérandes depuis cette mémoire temporaire. Suivant que l'on parle de registres d'échelonnement ou d'un tampon de ré-ordonnancement, le contournement ne se fait pas exactement de la même manière. Voyons pourquoi.
===Le contournement avec les registres d'échelonnage===
La première technique que nous allons voir est celle des registres d'échelonnage. Pour rappel, l'idée est que toutes les instructions prennent le même nombre de cycles d'horloge. Par exemple, toutes les opérations doivent faire 5 cycles. Si l'addition fournit son résultat en un cycle, il sera enregistré dans les registres avec un retard de quatre cycles. Le retard dépend de l’opération à effectuer, en fonction de combien de cycles elle prend dans l'ALU. En clair, toutes les instructions ont le même nombre d'étages, mais certains étages sont inutiles pour certaines instructions. L'instruction doit passer par ces étages, mais ceux-ci ne doivent rien faire.
Reste à ajouter des cycles de retard, qui servent juste à retarder l'enregistrement dans les registres. La solution la plus simple insére des ''staggering registers'', des '''registres d'échelonnage''', en sortie des ALU. Les registres d'échelonnage peuvent être regroupés dans un banc de registre unique.
[[File:Regsitres d'échelonnage.png|centre|vignette|upright=2|Regsitres d'échelonnage]]
[[File:Banc de registres d'échelonnage.png|centre|vignette|upright=2|Banc de registres d'échelonnage]]
Pour les pipelines de longueur fixe, le contournement ne doit pas uniquement relier la sortie de l'ALU, mais doit aussi tenir compte des registres d'échelonnage. En effet, imaginez qu'une instruction ait pour opérande le résultat d'une instruction exécutée deux cycles plus tôt. Il y a un délai de deux cycles entre les deux, mais l'ALU a fournit un résultat en un cycle d'horloge. Où est le résultat voulu ? Il n'est pas en sortie de l'ALU, mais dans le registre d'échelonnage juste après. Le contournement doit donc relier les registres d'échelonnage aux entrées des unités de calcul.
Les opérandes peuvent donc provenir de trois sources : des registres architecturaux, des registres d'échelonnage, du contournement direct (de la sortie aux entrées des ALUs). Toute la difficulté est de lire le bon registre d'échelonnage, ce qui demande de faire le lien entre registre architectural et registre d'échelonnage. Mais faire ainsi nous emmènerait assez loin. Si les registres d'échelonnage sont regroupés dans un banc de registres, l'usage de contournement de ce genre est potentiellement équivalent à une forme de renommage de registre ! Dans les faits, disons juste qu'il n'est pas souvent implémenté. A la place, le processeur détecte ce genre de dépendance entre instruction au décodage et bloque le pipeline si besoin.
===Le contournement avec un tampon de ré-ordonnancement===
Les processeurs modernes ajoutent un tampon de ré-ordonnancement, pour remettre les écritures dans les registres dans l'ordre du programme. Les données à écrire sont mise en attente dans le tampon de ré-ordonnancement, qui est une sorte d'hybride entre mémoire cache et mémoire FIFO.
[[File:Tampon de réordonnancement.png|centre|vignette|upright=2|Tampon de réordonnancement.]]
Les données stockées dans le ROB sont certes mises en attente, mais qu'il est possible que certains calculs en aient besoin. Par exemple, imaginons que le processeur veuille émettre une instruction, dont un des opérandes n'est pas encore dans les registres, mais est déjà dans le ROB. La réaction diffère selon les processeurs. Certains vont simplement bloquer l'instruction dans le pipeline et ne pas l'émettre. D'autres vont autoriser l'instruction à lire l'opérande depuis le ROB. Mais pour faire ainsi, il faut que le processeur implémente des techniques dites de renommage de registre que nous verrons dans quelques chapitres.
Avec un ROB, on conserve un réseaux d'interconnexions entre ALU pour gérer le contournement, mais on ajoute aussi des connexions entre ROB et ALU. Les connexions ajoutées permettent de récupérer les opérandes dans le ROB directement. Les comparateurs utilisés pour déterminer s'il faut contourner ou non sont fusionnés avec le ROB, ce qui colle bien avec le fait que c'est une mémoire associative. Nous ne détaillerons pas cette technique car il s'agit formellement d'une technique de renommage de registre, qu'on verra dans quelques chapitres en détail.
Les processeurs avec beaucoup d'ALU regroupent leurs ALU en paires ou groupes de 3/4 ALU, effectuer du contournement direct à l'intérieur de ces groupes, mais effectuent du contournement via le ROB entre ces groupes.
==L'émission anticipée des micro-opérations==
Idéalement, on voudrait démarrer une nouvelle instruction sur l'unité de calcul dès qu'elle est libre. Le problème est qu'il y a plusieurs étages entre l'unité d'émission et l'unité de calcul. Il y a au moins l'étage pour lire les registres, qui peut prendre plusieurs cycles. Il faut aussi gérer le contournement, ce qui peut prendre un cycle d'horloge. Un processeur a souvent entre un et à cinq étages entre la file d'attente et les unités de calcul. Sur le Pentium 4, on trouve 6 étages entre la fenêtre d’instruction et l'entrée de l'ALU.
Si une instruction réutilise le résultat d'une autre, cela peut poser problème. Imaginons que l'on ait trois cycles entre la file d'attente et les ALU. Si une instruction est émise quand l'opérande sorte de l'ALU, elle prendrait plusieurs cycles avant d'arriver à l'ALU et de s'exécuter. Et pendant ces cycles, l'ALU est inutilisée. Idéalement, elle aurait atteindre l'ALU immédiatement quand la première instruction en sort, pour profiter au mieux du contournement. La solution consiste à émettre des instructions en avance, le timing de l'émission est géré de manière à ce que l'instruction arrive à l'ALU pile au bon moment. La technique porte le nom d''''émission anticipée'''.
Le nombre de cycles d'avance dépend de l'opération, précisément du temps d'exécution de l'opération dans l'ALU. Le cas le plus simple est celui d'une opération qui s’exécute en un seul cycle d'horloge, comme une addition ou une soustraction. Le nombre de cycles d'avance est alors le nombre d'étages entre l'unité de calcul et la fenêtre d'instruction. Pour une instruction multicycle, il faut ajouter le nombre de cycles que l'opération passe dans l'ALU (moins un cycle). La conséquence est que cela ne marche que pour des instructions qui ont une durée fixe, connue à l'avance. Et autant c'est le cas pour les instructions arithmétiques et logiques, autant ce n'est pas le cas pour les accès mémoire.
===L'émission anticipée des accès mémoire===
Plus haut, nous avons vu comment appliquer le contournement à l'unité mémoire. Mais cela ne marche que dans un cas simple, où une lecture fait un cycle d'horloge. Dans les processeurs modernes, les accès mémoire ont une latence variable : quelques cycles pour un succès de cache, plusieurs dizaines de cycles pour un défaut de cache L1, pas loin de la centaine pour un défaut de cache L2, etc. Et cela pose des problèmes pour l'émission anticipée et le contournement en général.
L'émission en avance pour les accès mémoire est purement spéculative : le processeur émet des instructions en supposant que la lecture entrainera un succès de cache, quitte à corriger le tout en cas de défaut de cache. Un succès de cache prend un nombre fixe de cycles, connu à l'avance, ce qui fait qu'on peut appliquer l'émission anticipée dans ce cas. Les instructions lecture-dépendantes sont alors émises en tenant compte de la latence du cache L1. Pour l'exemple, disons que l'accès au cache L1 se fait en 3 cycles d'horloge : les instructions lecture-dépendantes sont alors émises de manière à arriver à l'entrée de l'ALU 3 cycles après le démarrage de la lecture. Si la lecture entraine un succès de cache, les instructions lecture-dépendantes s'exécutent normalement. Mais si un défaut de cache a lieu, les instructions lecture-dépendantes ont été émises à tord et on doit corriger la situation. Et pour corriger la situation, il y a plusieurs solutions.
Le pipeline RISC classique gèle totalement le processeur en cas de défaut de cache. Les instructions restaient à leur place, elles ne bougeaient pas, et le processeur reprenait là où il en était une fois le défaut de cache terminé. Mais cette solution, bien qu'idéale, n'est pas la plus pratique. Une solution alternative ne gèle que l'instruction dépendante sur place, dans le pipeline. L'instruction dépendante attend en entrée de l'unité de calcul que l'opérande nécessaire soit disponible, que le défaut de cache se passe. Le problème est que tout ce qui précède l'unité de calcul doit aussi être gelé : l'unité de calcul est indisponible tant que le défaut de cache n'est pas géré. Le défaut de cette solution est qu'elle est difficile à mettre en œuvre, sauf à geler totalement le processeur comme le faisaient les processeurs avec un pipeline RISC classique, comme dit plus haut.
Une autre solution annule toutes les instructions après celles émise à tort, avec les circuits pour les exceptions précises. Un défaut de cache déclenche une exception matérielle interne au processeur. Le processeur élimine alors les toutes instructions émises après le défaut de cache, dont les instructions lecture-dépendantes mais pas que. Une autre solution annule uniquement les instructions lecture-dépendantes émises à tord. Elle est appelée l''''annulation sélective'''. Elle permet de conserver de bonnes performances en cas d'émission anticipée fautive. Mais son implémentation est compliquée. Concrètement, seuls les processeurs à exécution dans le désordre l'implémentent, ce qui fait qu'on verra cela dans le chapitre adéquat. Cependant, il y a une solution qui permet l'émission anticipée, mais sans exécution dans le désordre, qui sera étudiée à la fin du chapitre.
Annuler et ré-exécuter des instructions émises à tort est assez lourd en terme de performance. Les techniques de '''prédiction de latence mémoire''' tentent de réduire les émissions anticipées fautives au maximum. Elles décident s'il faut ou non réveiller une instruction lecture-dépendante de manière anticipée. Les techniques en question sont assez complexes, mais elles essayent de prédire si une instruction va faire un défaut de cache ou non. Elles peuvent aussi tenter de prédire la durée de ce défaut de cache, mais c'est assez secondaire. Les techniques utilisées sont similaires à celles utilisées pour la prédiction de branchement : compteurs à saturation, prédiction statique, etc. Elles ne sont vraisemblablement pas utilisées dans les processeurs modernes. La raison est qu'elles demandent d'ajouter beaucoup de circuits pour des gains en performance assez limités.
===Le ''replay pipeline'' du Pentium 4===
Le '''pipeline à répétition''' (''replay pipeline'') est une implémentation de l'émission anticipée avec annulation sélective, qui a été utilisée sur le processeur Pentium 4. L'idée derrière les pipelines à répétition est d'ajouter une sorte de boucle, en plus du pipeline mémoire normal. Les instructions se propagent à la fois dans la boucle et dans le pipeline normal. Les étages de la boucle servent à ré-exécuter les instructions en cas de problème. La boucle propage les signaux de commande de l'instruction, sans rien faire de spécial. Dans le pipeline qui exécute l'instruction, ces signaux de commande sont consommés au fur et à mesure, ce qui fait qu'à la fin du pipeline, il ne reste plus rien de l'instruction originale. D'où la présence de la boucle, qui sert à conserver les signaux de commande.
L'étage final de la boucle vérifie que l'instruction n'a pas été émise trop tôt avec un ''scoreboard'', et il regarde si l'instruction a donné lieu à un défaut de cache ou non. Si l'instruction a donné un bon résultat, une nouvelle instruction est envoyée dans le pipeline. Dans le cas contraire, l'instruction refera encore un tour dans le pipeline. Dans ce cas, l'unité de vérification va devoir envoyer un signal à l'unité d'émission pour lui dire « réserve un cycle pour l'instruction que je vais faire boucler ».
[[File:Pipeline à répétition.png|centre|vignette|upright=2|Pipeline à répétition.]]
Un point intéressant est que les micro-opérations dépendantes de la lecture sont elles aussi exécutées en avance et ré-exécutées si besoin. Prenons l'exemple d'une lecture qui lit l'opérande manquante d'une addition. Vu qu'il y a a quelques cycles entre l'émission et les unités de calcul, le processeur émet l'addition en avance de quelques cycles, pour qu'elle arrive à l'ALU en temps voulu. En théorie, l'addition ne doit être lancée en avance que si on sait avec certitude que les opérandes seront lues une fois qu'elle arrive à l'ALU. Une addition dépendante d'une lecture doit donc attendre que la lecture termine avant d'être démarrée. Mais avec le système de ''replay'', l'addition est exécutée en avance, avant qu'on sache si ses opérandes sont disponibles. Si jamais un défaut de cache a lieu, l'addition aura atteint l'ALU sans les bonnes opérandes. Elle est alors r-exécutée par le système de ''replay'' autant de fois que nécessaire.
Le principe peut s'adapter pour fonctionner avec une hiérarchie de cache. Prenons un exemple : un succès dans le cache L1 dure 3 cycles d'horloge, un succès dans le L2 dure 8 cycles, et un défaut de cache 12 cycles. Imaginons qu'une instruction fasse un défaut de cache dans le L1, et un succès de cache dans le L2. La boucle de 3 cycles utilisée pour le L1 ne permettra pas de gérer efficacement la latence de 8 cycles du L2 : l'instruction devra faire trois tours, soit 9 cycles d'attente, contre 8 idéalement. La solution consiste à retarder le second tour de boucle de quelques cycles, ajoutant une seconde boucle. La seconde boucle ajoute en théorie un retard de 5 cycles : 8 cycles, dont trois en moins pour le premier tour. Pour injecter l'instruction dans la bonne boucle, il suffit d'un multiplexeur commandé par le signal ''cache hit/miss''.
: La seconde boucle peut être raccourcie pour une lecture car les micro-opérations dépendantes de la lecture sont émises en avance.
[[File:Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.png|centre|vignette|upright=2|Pipeline à répétition avec une latence de 3 cycles pour le L1, et 8 cycles pour le L2.]]
Le même principe peut s'appliquer pour gérer les latences avec des niveaux de cache supérieurs : il faut alors utiliser plusieurs boucles de tailles différentes, en ajoutant des multiplexeurs. Il arrive que plusieurs boucles veuillent faire rentrer une instruction dans le pipeline en même temps, au niveau de l'endroit où les boucles se referment. Dans ce cas, une seule boucle peut réémettre son instruction, les autres étant mises en attente.
Divers mécanismes d'arbitrage, de choix de la boucle sélectionnée pour l'émission, sont possibles : privilégier la boucle dont l'instruction est la plus ancienne (et donc la boucle la plus longue) est la technique la plus fréquente. Mais dans certains cas, mettre une boucle en attente peut bloquer tous les étages précédents, ce qui peut bloquer l'émission de la nouvelle instruction : le processeur se retrouve définitivement bloqué. Dans ce cas, le processeur doit disposer d'un système de détection de ces blocages, ainsi que d'un moyen pour s'en sortir et revenir à la normale (en vidant le pipeline, par exemple).
[[File:Pipeline à répétition avec hiérarchie de cache multiples et exceptions.png|centre|vignette|upright=2|Pipeline à répétition pour une hiérarchie de cache.]]
Pour gérer au mieux les accès à la mémoire RAM, on remplace la boucle dédiée à la latence mémoire par une FIFO, dans laquelle les instructions sont accumulées en attendant le retour de la donnée en mémoire. Quand la donnée est disponible, lue ou écrite en mémoire, un signal est envoyé à cette mémoire, et l'instruction est envoyée directement dans le pipeline. Là encore, il faut gérer les conflits d'accès à l'émission entre les différentes boucles et la file d’attente de répétition, qui peuvent vouloir émettre une instruction en même temps.
[[File:Gestion des accès RAM sur un pipeline à répétition.png|centre|vignette|upright=2|Gestion des accès RAM sur un pipeline à répétition.]]
On peut aussi adapter le pipeline à répétition pour qu'il gère certaines exceptions : certaines exceptions sont en effet récupérables, et disparaissent si on ré-exécute l'instruction. Ces exceptions peuvent provenir d'une erreur de prédiction de dépendance entre instructions (on a émis une instruction sans savoir si ses opérandes étaient prêts), ou d'autres problèmes dans le genre. Si jamais une exception récupérable a eu lieu, l'instruction doit être ré-exécutée, et donc réémise. Elle doit refaire une boucle dans le pipeline. Seul problème : ces exceptions se détectent à des étages très différents dans le pipeline. Dans ce cas, on peut adapter le pipeline pour que chaque exception soit vérifiée et éventuellement réémise dès que possible. On doit donc ajouter plusieurs étages de vérification, ainsi que de nombreuses boucles de réémission.
<noinclude>
{{NavChapitre | book=Fonctionnement d'un ordinateur
| prev=L'émission dans l'ordre des instructions
| prevText=L'émission dans l'ordre des instructions
| next=Les premiers processeurs Intel
| nextText=Les premiers processeurs Intel
}}{{autocat}}
</noinclude>
4ywm2n84pz452yrtpduu93bjbhja5h5
Wikilivres:Le Bistro/2026
4
83439
774490
774180
2026-10-09T14:46:53Z
Lionel Scheepmans
20012
/* Je ne suis donc pas content tous seul. (2) */ Réponse
774490
wikitext
text/x-wiki
<noinclude>{{Wikilivres:Le Bistro/En-tête}}</noinclude>
== Le meilleur à Wikilivres pour 2026 ! ==
Je crée la page du bistrot 2026 en transmettant mes vœux. Bonne année éditoriale à tous ! [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 janvier 2026 à 06:05 (CET)
:Merci, meilleurs vœux ! [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 16 janvier 2026 à 09:53 (CET)
::Meilleurs vœux pour 2026 !
::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 janvier 2026 à 20:13 (CET)
:::Meilleurs vœux pour 2026
:::[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 janvier 2026 à 11:56 (CET)
::::Bonne année et bonnes lectures et écritures !
::::[[Utilisateur:Matthius|Matthius]]
== Page orpheline du livre : États généraux du multilinguisme dans les outre-mer ==
Bonjour,
Le livre "États généraux du multilinguisme dans les outre-mer" a laissé quelques pages orphelines. Ces pages orphelines ont été recopiées ou regroupé dans le texte du livre. Elles font donc doublons.
Je montre dans ce livre [[Wikilivres:Feuilles dupliquées orphelines]] cette suite de pages orphelines accompagnée par la page où elles ont été recopiées.
Merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 10 février 2026 à 19:57 (CET)
:@[[Utilisateur:Xhungab|Xhungab]], @[[Utilisateur:JackPotte|JackPotte]], @[[Utilisateur:DavidL|DavidL]], bonjour. Je vois que l'on est en campagne de gestion de pages orphelines et je me pose la question de savoir si on doit les supprimer une fois réintégrées ailleurs ou les supprimer ? Je vois que les deux cas de figure ont été mis en œuvre précédemment. L'avantage de la fusion est de conserver l'historique des modifications, mais je ne suis pas habitué à le faire. Si quelqu'un d'entre vous pouvait m'indiquer une page d'explication, cela m'aiderait à m'y mettre. Ensuite, je me demande si la fusion a vraiment un sens lorsque c'est la même personne qui a créé la page orpheline et la nouvelle au contenu copié collé pour insertion ? Qu'en pensez-vous ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:02 (CET)
:::Ça fait plaisir de voir une personne s'investir dans la maintenance de Wikilivres =). Soit le ou la bienvenu.e. J'attends les avis des autres administrateurs pour te donner mon soutien @[[Utilisateur:Xhungab|Xhungab]]. @ bientôt. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:27 (CET)
:::: @[[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]] bonjour, je n'ai pas compris en quoi "deux cas de figure ont été mis en œuvre précédemment" : si la page est vide on la supprime et si elle a au moins une phrase valable on la fusionne. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 février 2026 à 16:26 (CET)
::::Concernant l'auteur pour moi cela importe peu, car on souhaite conserver chaque élément permettant de prouver une primauté du droit d'auteur (par exemple pour ne pas accuser un contributeur d'avoir copié un site miroir de sa page supprimée). [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 février 2026 à 16:30 (CET)
:::::Désolé [[Utilisateur:JackPotte|JackPotte]], je n'avais pas vu que les pages que tu as supprimées étaient vides. La règle est donc supprimer si vide et fusionner si non. Tu peux me conseiller une page d'aide pour oppérer les fusions ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 13 février 2026 à 16:34 (CET)
::::::Salut,
::::::Pour la fusion d'historique :
::::::* soit [[Wikilivres:Le guide de l'administrateur#Fusionner deux pages|Utiliser la page spéciale pour cela]] (cependant ne fonctionne pas toujours),
::::::* soit [https://fr.wikibooks.org/w/index.php?title=Wikilivres:Le_guide_de_l%27administrateur&oldid=588957#Fusionner_deux_pages utiliser l'ancienne méthode] qui reste toujours valable quand la première ne fonctionne pas.
::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 13 février 2026 à 19:34 (CET)
:::::::Salut @[[Utilisateur:DavidL|DavidL]]. Je viens de tester l'outil fusionner avec les deux pages suivantes
:::::::[[États généraux du multilinguisme dans les outre-mer/Annexes/Contexte]]
:::::::vers
:::::::[[États généraux du multilinguisme dans les outre-mer/Annexes]]
:::::::Et j'ai le message suivant :
:::::::« La période spécifiée chevauche les versions préexistantes de la page de destination. »
:::::::Tu peux m'expliquer si tu as le temps ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 février 2026 à 00:19 (CET)
::::::::Salut @[[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]]
::::::::Dans ce cas, j'utilise [https://fr.wikibooks.org/w/index.php?title=Wikilivres:Le_guide_de_l%27administrateur&oldid=588957#Fusionner_deux_pages l'ancienne méthode].
::::::::# Renommer A (page qui n'existera plus) vers B, sans laisser de redirection, et en cochant la case de suppression de B (case qui apparait après 1er clic sur le bouton Renommer) (→ l'historique de A est sur B, celui de B est supprimé)
::::::::# Supprimer B (→ l'historique de A et B sont supprimés)
::::::::# Restaurer B et cocher toutes les cases des versions (bouton "Inverser la sélection") (→ restaurer l'historique de A et B)
::::::::# Effectuer une modification de la version finale de la page à afficher, trouvée dans l'historique.
::::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 16 février 2026 à 08:29 (CET)
:::::::::Merci @[[Utilisateur:DavidL|DavidL]]. Ça m'a l'air bien compliqué tout ça. Et avec toute les pages qu'il faut faire, ça risque de prendre un certain temps. Je manque de courage en pensant que c'est juste pour ajouter une ligne dans l'historique des versions. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 février 2026 à 14:51 (CET)
::::::::::Finalement, dans l'exemple que j'ai pris, la fusion n'avait aucun sens puisque le contenu du doublon que j'ai finalement supprimé était intégralement repris dans l'autre doublon par le même auteur. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 22 août 2026 à 23:55 (CEST)
:::::::::::Désolé @[[Utilisateur:Xhungab|Xhungab]]. Je ne comprends rien à l'outil de fusion et je ne trouve rien qui me permet de comprendre sur le net. Une vidéo avec exemple des différents cas de figure serait bien, mais je ne trouve pas. Je vais donc laisser ce travail à ceux qui savent utiliser l'outil.
:::::::::::En revanche, j'ai remarqué que certains doublons, comme dit précédemment, ne nécessitent pas de fusion puisqu'il s'agit de pages qui ont été créées sans aucune autre modification, puis, copiées par le même auteur dans une autre page créée avec un autre nom, au lieu de faire un renommage. Il faudrait donc que je prenne le temps de parcourir la liste pour repérer ces cas de figure avant de supprimer le premier doublon. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 01:27 (CEST)
::::::::::::Merci pour ton aide. Actuellement ces pages ont été en quelque sorte neutralisées. Donc fais de ton mieux, mais ne te tracasse pas elles ne posent plus de problème. Encore merci [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 09:13 (CEST)
:::::::::::::OK @[[Utilisateur:Xhungab|Xhungab]], merci pour l'info. Au fait comment as-tu fais pour les détecter ? En parcourant les pages de la catégorie orphelines et en vérifiant celles qui sont doublons ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 12:36 (CEST)
::::::::::::::Dans la catégorie pages orphelines, il y avait un lien sur une page.
::::::::::::::Je choisissais une phrase et je recherchais avec la fonction recherche de wikibook où se trouvait cette phrase. Si c'était un doublon, j'avais un lien vers la nouvelle page. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 12:48 (CEST)
:::::::::::::::Intelligent. Tu poserais pas ta candidature d'admin !? Ici, avec @[[Utilisateur:Fourmidable|Fourmidable]] et puis sur wikiversité aussi. Ça pourrait aider pour le projet des 20 ans., si tu comote toujours t'investir. On est en sous effectif dans les deux projects et les communautés me semblent plus ouverte que sur Wikipédia. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 13:48 (CEST)
::::::::::::::::Je ne souhaite pas devenir un administrateur. Pourquoi ? Si on me donne du pouvoir, je l'utiliserais. Par la suite, ceux qui m'ont accordé le pouvoir auront le choix entre pleurer, nous quitter ou me mettre en arrêt de travail. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 14:59 (CEST)
:::::::::::::::::Ah ah ah. T'es si foireux que ça !? Ou peut-être as-tu eu des expériences antérieures ? Dans tous les cas, c'est super de ta part de pouvoir t'auto évaluer.
:::::::::::::::::Ceci dit, le système de demander au administrateurs de faire des tâches qu'il faut expliquer est une perte de temps et d'efficacité. On pourrait d'ailleurs penser à attribuer les droits temporairement pour effectuer certaines tâches. Je me demande si ça se fait déja quelque part ? Si oui, on pourrait s'en inspirer pour mettre le système en application sur Wikilibres et Wikobersité. T'en penses quoi @[[Utilisateur:Xhungab|Xhungab]] ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 23 août 2026 à 15:40 (CEST)
::::::::::::::::::Oui, je pourrais obtenir des droits temporaires par rapport à un projet. Par exemple 2 projets
::::::::::::::::::Premier projet : Supprimer toutes les feuilles volantes. Ce sont des feuilles tests très bien faites mais qui n'appartiennent à aucun livre. Je crois qu'il y a actuellement, Feuilles volantes – 54 P.
::::::::::::::::::
:::::::::::::::::: Deuxième projet : [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Pages_anciennes Les pages les plus anciennement modifiées]
::::::::::::::::::Supprimer tous les livres entre 2006 et 2010 qui sont à l'abandon. Parfois il sera possible de sauvegarder un livre en supprimant les chapitres vides, ce qui nous permettra de créer un livre complet. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 23 août 2026 à 16:19 (CEST)
Donc, tu as du temps à consacrer à ce genre de maintenance ? Ce serait bête de ne pas en profiter. Reste à voir à présent si les autres administrateurs et bureaucrates seraient d'accord de t'octroyer les droits, tout en approuvant les deux projets que tu proposes. Cependant, je ne suis pas un grand fan des suppressions, je préfère de loin renommer les pages pour les placer en dehors de l'espace principal de sorte à ce que les auteurs puissent toujours les retrouver. Pour ça, un bon espace, c'est les sous-pages des principaux auteurs. Cela pourrait ainsi s'appliquer aux feuilles volantes et aux projets d'écriture abandonnés qui sont suffisamment développés pour justifier une conservation. L'avantage de cette méthode est qu'elle ne demande pas d'avoir les outils d'administrateurs. Faudrait voir maintenant dans quel cas on supprime et dans quel cas on renomme. On pourrait par exemple définir un nombre d'octets ou de caractères, question d'avoir un critère standard et accepté par tous. Qu'en penses-tu [[Utilisateur:Xhungab|Xhungab]] ?
:En fait je préfère continuer comme actuellement. Proposer mes services pour améliorer wikibook sans avoir aucun droit. De cette manière, si je propose quelque chose d'incongru, comme cela est déjà arrivé, il suffit de neutraliser ma demande. Merci à ceux qui me suivent sur ce travail.[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 24 août 2026 à 01:52 (CEST)
:Je comprends @[[Utilisateur:Xhungab|Xhungab]]. Mais précisément, déplacer une page à l'abandon dans une sous-page l'espace utilisateur de l'auteur principal, cela ne demande justement aucun droit et tout le monde peut le faire, toi en premier puisque tu sembles actif à ce niveau. C'est pour ça que je te demande ton avis : que penses-tu de l'idée de déplacer les pages comme je viens de le décrire, plutôt que de les supprimer ? Je viens de soumettre l'idée dans une nouvelle discussion ci-dessous. Tu peux donner ton avis là-bas si tu veux. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 02:27 (CEST)
== archive.today ==
''Voir [[:w:Wikipédia:Le Bistro/21 février 2026#archive.today]].''
Ce site pose apparemment des problèmes de sécurité, il faudrait le remplacer au plus vite si possible, et sinon désactiver les liens.
[[Utilisateur:SyntaxTerror|SyntaxTerror]] ([[Discussion utilisateur:SyntaxTerror|discussion]]) 21 février 2026 à 13:07 (CET)
:Salut SyntaxTerror,
:* 0 liens vers archive.today : [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.today]
:* <s>18</s> 0 liens vers archive.is [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.is]
:* 0 liens vers archive.li [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.li]
:* 0 liens vers archive.ec [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.ec]
:* 0 liens vers archive.ph [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.ph]
:* 0 liens vers archive.fo [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.fo]
:* 0 liens vers archive.md [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.md]
:* <s>1</s> 0 liens vers archive.vn [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.vn]
:* 0 liens vers archive.closed.social [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Recherche_de_lien?target=archive.closed.social]
:-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 21 février 2026 à 16:14 (CET)
::Merci pour vos efforts ! [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 12 mai 2026 à 16:22 (CEST)
== demande aide pour sortir du mode "ébauche" ==
je viens de terminer mon livre ... ci dessous. J'ai compris qu'il fallait le signaler comme n'étant plus au stade "ébauche" commente faire Merci
https://fr.wikibooks.org/w/index.php?title=Essai_pour_un_mod%C3%A8le_de_psychisme_objectif&action=info#mw-pageinfo-header-basic [[Utilisateur:Clopeau|Clopeau]] ([[Discussion utilisateur:Clopeau|discussion]]) 17 mars 2026 à 08:20 (CET)
:Bonjour, quand je regarde ''[[Essai pour un modèle de psychisme objectif]]'', sa complétude permettrait en effet de le sortir des ébauches, mais il semble plutôt d'agir d'un travail de recherche que d'un livre pédagogique sur un sujet sourcé comme reconnu.
:Donc en vertu de [[Wikilivres:Principes fondateurs|notre charte]], je propose de le déplacer sur {{WV|Recherche:Accueil}}. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 18 mars 2026 à 09:06 (CET)
== Don à wikipédia depuis wikilivres ==
{{BlocCitation|La Wikimedia Foundation est l’organisation à but non lucratif qui soutient Wikipédia, '''les autres sites de connaissance libre de Wikimédia''', et sa mission de connaissance libre pour tous.|auteur=[https://wikimediafoundation.org/fr/give/donor-frequently-asked-questions/ FAQ], {{g|Qu'est-ce-que la Wikimedia Foundation ?}}}}
Le lien de donation https://donate.wikimedia.org/w/index.php?title=Special:LandingPage&country=FR&uselang=fr&wmf_medium=sidebar&wmf_source=donate&wmf_campaign=fr.wikibooks.org '''ne parle que de wikipédia'''. '''Où sont passés les autres projets ?''' Vont-ils être fermés sauvagement par la fondation comme Wikinews récemment ? La suite au prochain épisode d{{'}}''Highlander-Wikimedia'' : {{g|À la fin, un seul ''projet'' survivra.}} ... ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 12 mai 2026 à 11:26 (CEST)
:Bonjour,
:Je pense personnellement que les wikis ont des problèmes.
:Il y a une vingtaine d'années, il représentait l'innovation la plus ambitieuse.
:Aujourd'hui, les wikis me semblent dépassés de tous les côtés.
: wiki = Erreurs 404 ("Page introuvable")
:Problèmes:
:* 568 livres déclarés. 50 livres terminés (combien sont obsolètes)
:* 21 527 pages déclarées. 20 000 pages orphelines. (Papa t'est où?)
:Solution:
:* Où sont les cours sur YouTube qui utilisent un wikibook comme référence?
:Voilà mon idée sur les wiki.
:* https://fr.wikiversity.org/wiki/Facult%C3%A9:Droit
:* https://fr.wikiversity.org/wiki/D%C3%A9partement:Introduction_au_droit
:<nowiki>~~~~</nowiki> [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 12 mai 2026 à 13:50 (CEST)
::Ce serait intéressant de mentionner tous les projets actifs oui. [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 12 mai 2026 à 16:23 (CEST)
::: En même temps, le message est plutôt franc étant donné que l'argent des dons n'est pas investi dans le développement de Wikilivres (cf [[:meta:Community Wishlist Survey]] et les réponses à nos tickets Phabricator).
::: Par ailleurs, concernant la qualité des contenus, cela a toujours était un sujet depuis le début, et nous disposons tout de même de plusieurs pages spéciales dans [[Wikilivres:Maintenance]] pour y remédier afin que le lecteur n'ait pas la sensation de se faire empapaouter en se retrouvant sur des ébauches bien placées dans Google. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 13 mai 2026 à 08:48 (CEST)
== La catégorie SI est pleine ! ==
Bonjour tous,
Pour info, la [[:Catégorie:Suppressions immédiates demandées]] contient 9 pages.
Wikilivresquement, [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 14 juin 2026 à 15:36 (CEST)
:Elle est maintenant vide, merci à ceux qui s'y sont collés ! {{Sourire}} [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 24 juin 2026 à 17:17 (CEST)
== Un RAW bien frais pour juillet ==
{| style="width:100%;"
| valign="top" align="center" style="border:1px gray solid; padding:1em;" |
{| align="center"
|-
| style="text-align: center;" | <div style="background-color:#177860; border-radius:.2em; color:#FFF;padding: 1em;">
<div style="font-family: Century Gothic; font-size:4.5em; line-height:120%;text-align:center;color: #fff;">RAW</div>
<div style="margin-bottom:1.5em;text-align:center; color: #fff; font-style:italic;">Regards sur l’actualité du mouvement Wikimédia.</div>
<div style="text-align: center;">{{#ifeq:{{FULLPAGENAME}}|Wikipédia:RAW/Rédaction}}</div>
</div><br />
<hr />
<div style="font-size:12pt; font-family:Times New Roman; text-align:center;">[[w:fr:Wikipédia:RAW/2026-07-05|<span style="color:darkslategray;">Le numéro de juillet 2026 est sorti.</span>]]</div>
<hr /><br />
|- style="text-align: center;"
| <span style="font-size:12pt; font-family:Times New Roman;"> '''❖ Au menu de ce numéro ❖'''</span>
|- style="font-size:10pt; font-family:Times New Roman; text-align:center;"
| <div style="text-align:center; column-count:1; column-width:28em; vertical-align:top;">
'''L'Édito'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'Édito|Fêtons notre mouvement !]]
'''Échos francophones'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Résidence|Un nouveau vigneron dans la résidence]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Logo_WP25|Wikipédia en français aux couleurs des 25 ans]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#DAF|Wikipédia dans le ''Dictionnaire de l'Académie française'']]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Millésime_Wiktionnaire|Quelles sont les nouvelles entrées en français sur le Wiktionnaire ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#ADS|Relance de l'Article de la semaine]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#sans_pagEs|Le projet des sans pagEs fête ses dix ans]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#contributions_rémunérées|Faut-il faire évoluer les règles sur les contributions rémunérées ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#wikification|Le mois de la wikification fait son retour pour une septième édition]]
'''Ailleurs dans le mouvement'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Txikipedia|Txikipedia franchit le cap des 10 000 articles]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Concours_santé_2026|Un concours sur la santé]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Commons_et_IA|Commons se penche sur l'IA]]
'''Nouveautés techniques et outils'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#VIZWP|VIZWP : un outil pour visualiser les lacunes dans les sujets climatiques sur Wikipédia, mais pas que…]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#AI_Source_Verification|Examiner la vérifiabilité grâce à l'IA ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Icônes|Les icônes de l'interface ont changé]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Sous-références|Ça y est, les sous-références sont là]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#ConvoCompass|ConvoCompass, pour moins d'attaques personnelles]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#LSV-Tools|LSV-Tools : faciliter la gestion des anecdotes « Le saviez-vous ? »]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#LegendAndDot|Semi-automatiser l'ajout de ponctuation dans les légendes d'images]]
'''Du côté de la recherche'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#COMSAV|COMSAV : la contribution à Wikipédia fait-elle d'un ensemble d'individus un ensemble de pairs ?]]</br>
[[w:fr:Wikipédia:RAW/2026-07-05#JourneyMap|Où commence la contribution et où s'arrête-t-elle ?]]
'''Le POV'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Le_POV|Guide survie pour la Wikimania 2026]] par Trizek
'''L'Atelier'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'Atelier|Le point sur la diversité de genre dans les articles]] par PAC2
'''L'Analyse'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'Analyse|Larry Sanger is back... and fired!]] par Madelgarius, Trizek et Jules*
'''L'interview'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#L'interview|Pronoia : de Byzance aux réseaux sociaux, un œil sur Wikipédia et le monde]] par Antimuonium
'''Le wik’hit-parade'''</br>
[[w:fr:Wikipédia:RAW/2026-07-05#Wikit|Articles les plus vus en juin 2026]] par Ælfgar
</div>
|-
| style="font-family:Times New Roman; text-align:center; font-size:90%;" | [[w:fr:Wikipédia:RAW/2026-07-05|Lire tout le numéro]]
|-
| valign="top" colspan="2" style="padding:0.5em; font-family:Times New Roman;text-align:center; font-size:100%;" |
Venez rédiger le prochain numéro, y parler de ce qui se fait dans votre communauté : [[w:fr:Discussion_Wikipédia:RAW/Rédaction|Salle de rédaction]].</br>Les anciens numéros sont retrouvables [[w:fr:Modèle:Palette RAW|ici]].
|-
|}
|}
<div style="margin-top:10px; font-size:90%; padding-left:5px; font-family:Georgia, Palatino, Palatino Linotype, Times, Times New Roman, serif;">[[w:fr:Wikipédia:RAW/Inscription|Abonnez-vous !]] [[Utilisateur:L'embellie|L'embellie]] ([[Discussion utilisateur:L'embellie|discussion]]) 5 juillet 2026 à 01:53 (CEST)</div>
== Les RAW en mode fiesta sur la Wikimania ==
<div style="background-color:#177860; border-radius:.2em; color:#FFF;padding: 1em;">
<div style="font-family: Century Gothic; font-size:3em; line-height:120%;text-align:center;color: #fff;">RAW</div>
<div style="margin-bottom:1.5em; margin-top:1em; text-align:center; color: #fff;">Le numéro d'août 2026 (n°295) est sorti.</div>
<div class="center">[[:w:Wikipédia:RAW/2026-08-05|<span class="cdx-button cdx-button--fake-button cdx-button--fake-button--enabled" style="white-space:break-spaces; text-align:center; gap:0;"role="button" aria-disabled="false">Lire l'infolettre</span>]]</div></div>
'''Un numéro exceptionnel'''
La conférence Wikimania s'est achevée le 25 juillet. Que vous ressentiez de la frustration (si vous n’avez pas pu y assister) ou du [[:w:Wikipédia:Wikispleen|wikispleen]] (si vous y étiez et que vous avez du mal à digérer que ce soit déjà fini), l’équipe des RAW n’a pas ménagé ses efforts pour vous proposer un numéro XXL qui revient en long, en large et en travers sur cet événement qui le mérite bien. Comptes-rendus, témoignages, entretiens, il y en aura pour tous les goûts ! Le numéro est aussi exceptionnel par le nombre de rédactrices et de rédacteurs qui y ont participé {{incise|merci !|stop}}
Au-delà de Wikimania, l’actualité wikimédienne est loin d’être paisible cet été. Nous vous proposons donc aussi de revenir en détail sur la crise qui secoue la fondation Wikimédia autour de la reconnaissance du syndicat Wiki Workers United (WWU). Nous aurons probablement l’occasion de couvrir la suite de cette histoire dans le prochain numéro. D’ici là, bonne lecture !
----
Au menu de ce numéro :
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">En bref</span>''' — Des actualités francophones et du mouvement Wikimédia et des nouveaux outils
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">Dossier spécial Wikimania</span>'''
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">Les POV</span>'''<br>— Ma Wikimania : la joie, l'inquiétude et la raison d'être<br>— Témoignages : regards sur la Wikimania
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">L'Analyse</span>''' — La crise s'intensifie après le refus de la WMF de reconnaître le syndicat WWU
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">Les Comptes-rendus</span>'''<br>— Une Wikimania sous le signe de la jeunesse<br>— Sur les conséquences de la définition de « wikimédien·ne »<br>— Petit tour d’horizon de la créativité wikimédienne<br>— CultureStrong : respecter les savoirs des Premières Nations sur les plateformes Wikimédia<br>— Des événements techniques à Wikimania
'''<span style="margin:0;background:var(--background-color-success-subtle, #dff2eb);padding:3px;color:black">Les Interviews</span>'''<br>— Ses goûts, sa Wikimania, sa prochaine tournée... : Baby Globe nous dévoile tout !<br>— Jules*, notre Wikimédien de l'année : comment rendre compte du changement climatique tout en protégeant l'encyclopédie
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">Le wik'hit parade</span>''' — Une analyse des articles les plus vus le mois dernier
[[Fichier:Toicon-icon-sharp-corners-draw.svg|20px|classe=skin-invert-image]] Les RAW, c'est aussi vous : si un sujet vous tient à cœur, n'hésitez pas à proposer une idée d'article ou à participer à la rédaction du [[:w:Wikipédia:RAW/Rédaction|prochain numéro]]. Toutes les contributions sont les bienvenues, quel que soit votre wiki d'origine ! <span class="smiley">[[Image:Face-smile.svg|20px|Sourire|alt=Émoticône sourire]]</span>
[[Utilisateur:Antimuonium|Antimuonium]] ([[Discussion utilisateur:Antimuonium|discussion]]) 5 août 2026 à 00:28 (CEST)
== Bientôt les 20 ans de Wikiversité ==
Bonjour.
Un petit message de « débauchage » dans le cadre d'un projet d'amélioration de Wikiversité. Comme dit la chanson : « On n'a pas tous les jours vingt ans ! » Plus d'infos [[v:Wikiversité:La_salle_café/août_2026#Améliorer_Wikiversité_pour_ses_vingt_ans_d'existence|dans la salle café du projet]].
Belle fin de journée à tous ! [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 16 août 2026 à 00:26 (CEST)
== [[:Catégorie:Suppressions immédiates demandées|Série de demandes de suppression immédiate]] ==
Bonjour,
La [[:Catégorie:Suppressions immédiates demandées]] est de nouveau pleine, mais cette fois, il semble qu'elle ait été remplie par le seul {{U|Xhungab}}.
Cordialement, [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 16 août 2026 à 14:56 (CEST)
:{{fait}} Bonjour, oui c'était effectivement un ensemble d'ébauches abandonnées. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]]) 17 août 2026 à 10:38 (CEST)
::Merci pour votre aide [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 août 2026 à 14:26 (CEST)
== Deux idées à débattre ==
Bonjour.
Vu le travail de traitement des pages et livres abandonnés et suite à une discussion avec @[[Utilisateur:Xhungab|Xhungab]] reprise ci-dessous, j'aimerais soumettre deux idées à la communauté Wikilivres.
'''La première idée''' est de déplacer toutes les pages et livres dont le développement est abandonné depuis longtemps en sous-page de l'auteur principal tout en laissant un message sur sa page de discussion avec un lien pointant vers la sous-page en question. Ce message pouvant faire l'objet d'un modèle.
Avantage : 1. Pas besoin d'être admin pour faire ce genre de maintenance. 2. Pas de risque de frustrer un auteur ni de perdre de vue des informations intéressantes. 3. Ceci dans le but d'améliorer l'apparence de Wikilivres au niveau du contenu visible sur l'espace de nom principal.
Nécessité : 1. Créer un modèle à placer sur la page de discussion de l'auteur principal après déplacement. 2. Définir la durée d'abandon des pages à déplacer.
'''La deuxième idée''' est de donner les outils d'administrateurs à des personnes qui ne veulent pas endosser cette responsabilité à long terme et de manière continue, mais qui sont d'accord d'en faire usage dans le cadre d'un projet de maintenance temporaire. Cela pourrait être utile pour @[[Utilisateur:Fourmidable|Fourmidable]] s'il le souhaite, à moins qu'il se décide à déposer sa candidature pour une nomination définitive. Ce qui serait une bonne idée, je trouve.
Qu'en pensez-vous @[[Utilisateur:JackPotte|JackPotte]], @[[Utilisateur:DavidL|DavidL]], @[[Utilisateur:Grondin|Grondin]] ?
Une prise de décision est-elle nécessaire si on décidait une mise en application ?
Bien à vous tous, [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 01:50 (CEST)
:Bonjour,
:J'ai proposé une liste de livres à supprimer
:https://fr.wikibooks.org/wiki/Wikilivres:Demande_de_suppression_imm%C3%A9diate
:Doit-on réellement les garder ? ;) [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 24 août 2026 à 10:21 (CEST)
::Quel est l'avantage de la suppression par rapport au déplacement @[[Utilisateur:Xhungab|Xhungab]]? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 10:56 (CEST)
:::En déterminant un contenu minimum, on peut faire le tri de ce que l'on supprime et ce que l'on déplace. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 24 août 2026 à 10:58 (CEST)
::::Bonjour,
::::Les droits administrateurs temporaires sont difficiles à gérer : un bureaucrate peut attribuer le status administrateur mais pas le retirer.
::::Concernant les critères suppression ou renommage, je propose :
::::* (cas 1) Une page qui ne contient vraiment aucun texte (ex : seulement des liens rouges) --> suppression
::::* (cas 2) Une page avec quelques phrases significatives récupérables --> soit (a) tenter de fusionner dans un livre qui aborde le même sujet ou sujet connexe, soit (b) renommer en sous-page de l'auteur si aucun livre pour la fusion ne convient
::::* (cas 3) Une page avec plus de contenu que (cas 2) --> conserver
::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 24 août 2026 à 19:33 (CEST)
:::::Salut @[[Utilisateur:DavidL|DavidL]]. Oui, c'est raisonnable. Reste que la fusion, c'est compliqué. J'ai essayé, mais je ne comprends rien. Une vidéo m'aiderait à comprendre, mais j'en ai pas trouvé. Ensuite, si on garde, genre un livre abandonné depuis plus de trois ans avec un chapitre entamé sur cinq prévus, par exemple, on le laisse dans l'espace principal, ou on le déplace vers l'espace utilisateur de l'auteur ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 25 août 2026 à 00:38 (CEST)
::::::Pour ce cas je propose de le fusionner si possible, comme (cas 2)(a) ; sinon le conserver, comme (cas3) afin qu'un utilisateur motivé puisse reprendre le livre.
::::::-- ◄ [[Utilisateur:DavidL|'''D'''avid '''L''']] • [[Discussion Utilisateur:DavidL|discuter]] ► 25 août 2026 à 08:36 (CEST)
:::::::Ok. Es-tu à l'aise avec la fusion @[[Utilisateur:DavidL|DavidL]] ? Car c'est une solution intéressante, mais je n'y arrive pas. Il y a toujours un message que je ne comprends pas et suite à quoi je préfère arrêter. Serais-tu dispo pour m'expliquer comment faire durant un appel vidéo ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 01:30 (CEST)
::::::::Bonjour {{Mention|Lionel Scheepmans}}, je suis très favorable à tes deux idées ! [[Utilisateur:Fourmidable|Fourmidable]] ([[Discussion utilisateur:Fourmidable|discussion]]) 26 août 2026 à 15:39 (CEST)
:::::::::Ok @[[Utilisateur:Fourmidable|Fourmidable]], je me dis qu'il serait encore bien d'avoir les avis de @[[Utilisateur:JackPotte|JackPotte]], voir de @[[Utilisateur:Grondin|Grondin]], s'ils trouvent le temps nécessaire pour le faire. Et puis, on peut penser tout doucement à créer une page pour fixer des décisions en votant si nécessaire pour confirmer l'adhésion générale ou grandement majoritaire.
:::::::::C'est curieux, je voulais travailler sur Wikiversité et finalement je me trouve plus actif sur Wikilivres. Mais les enjeux sont les mêmes et beaucoup de personnes actives ici le sont aussi sur Wikiversité, y compris parmi les administrateurs. En avançant dans Wikilivres, j'ai donc aussi l'impression que l'on avance sur Wikiversité.
:::::::::Au fait, fourmidable ? Tu ne serais pas intéressé, toi, de prendre un balai dans Wikilivres ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 17:27 (CEST)
== Demande d'aide pour amélioré Wikbook. Merci ==
* Actuellement j'ai proposé mes services pour améliorer wikibook. J'ai créé un partenariat informel avec des administrateurs, je propose des pages et des livres à supprimer. Ils décident soit de suivre ma demande, soit de la neutraliser. Je les remercie pour leur patience et leur aide.
- https://fr.wikibooks.org/wiki/Wikilivres:Demande_de_suppression_imm%C3%A9diate
*Certain livres semblent trop avancé pour suivre cette procédure.
- https://fr.wikibooks.org/wiki/Wikilivres:Demandes_de_suppression/2026
Dans cette page, je propose des livres trop avancés pour être traités rapidement. Il faut un vote pour que les demandes soient acceptées.
Aujourd'hui j'ai besoin de vous pour faire avancer mes propositions, sur des livres peut-être intéressants mais abandonnés depuis plus de trois ans, cinq ans, dix ans, vingt ans.
Si vous appréciez mon dévouement pour Wikibook et que vous souhaitez m'aider, ''il suffit chaque semaine de rajouter la commande'' {{VoteSupprimer}} ''sur les livres que je propose''. '''Entré {{}} puis à l'intérieur des parentthèses VoteSupprimer''' et signer votre accord. Merci pour votre aide. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 25 août 2026 à 10:33 (CEST)
:Salut @[[Utilisateur:Xhungab|Xhungab]], merci à toi pour ton engagement ! J'ai supprimé les pages qui étaient pratiquement vides dans ta liste. J'ai laissé les autres car la proposition de fusion est invoquée par @[[Utilisateur:DavidL|DavidL]] pour les pages avec du contenu. Malheureusement , je ne comprends pas comment fonctionne l'outil de fusion. J'espère que quelqu'un voudra bien m'expliquer le fonctionnement via un appel vidéo. Car je ne comprends pas les explications écrites. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 01:53 (CEST)
::Bonjour, Merci pour ton aide.
::.
::* J'ai travaillé sur la liste des pages les plus anciennement modifiées de l'année 2006. J'ai proposé tous les livres en errance à la suppression par vote.
::- https://fr.wikibooks.org/wiki/Wikilivres:Demandes_de_suppression/2026
::Je vais attendre qu'elle soit traitée. Soit en supprimant ces livres soit en supprimant ma demande. Ensuite je ferai l'année 2007.
::.
::*Tu m'a posé une question.
::Quel est l'avantage de la suppression par rapport au déplacement ?
::Je vais essayer de te répondre dans mon prochain message. (Ce sera juste un délire d'un mois d'août un peu chaud). [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 09:20 (CEST)
:::C'est motivant de voir des gens motivés =)
:::Prend ton temps, il n'y a aucune échéance dans les projets Wikimédia, c'est un processus en continu qui peut s'étendre à plusieurs générations... Et si c'est plus facile pour toi de passer à l'oral, sache que je suis partant pour une discussion audio ou vidéo. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 13:35 (CEST)
== J'ai fait un rêve. ==
J'ai fait un rêve.
Un lieu où des amateurs passionnés proposeraient de partager leurs travaux avec des étudiants curieux. Pas les meilleurs travaux du monde. Mais pas les pires non plus. Un endroit où les anciens passeraient le relais à ceux qui présentent leurs avenirs.
Ce serait une petite bibliothèque avec une cinquantaine de livres disponibles à l'étude. Cinq ou six livres en construction que les étudiants pourraient travailler avec l'auteur. Tous les ans quelques livres supplémentaires viendraient enrichir ce lieu d'étude.
Aujourd'hui, il existe Wikibook. Un lieu où des auteurs malveillants prouvent que le travail collaboratif ne fonctionne pas. Ils créent des livres parfaitement construits de quelques chapitres puis les abandonnent sans scrupule. Ces livres moisissent depuis cinq, dix, quinze, vingt ans.
Wikibook c'est:
* La bibliothèque de livres pédagogiques libres que chacun peut améliorer.
* 569 livres contenant 22 112 pages.
.
* 569 livres dont 450 livres à l'abandon depuis cinq, dix, quinze, vingt ans.
Oui, aujourd'hui Wikibook peut changer. Wikibook va changer. Wikibook pour les étudiants va renaître de ses cendres. Les cendres des vieux livres abandonnés. Les auteurs devront faire honnêtement leur travail, ou les étudiants feront le leur.
Ceci n'est juste qu'un rêve dans un mois d'août un peu plus chaud que d'habitude.
Merci pour m'avoir suivi jusqu'ici. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 10:13 (CEST)
:Je trouve que c'est un très beau rêve @[[Utilisateur:Xhungab|Xhungab]]. Dans ce rêve, on peut voir la partie remplie du verre au lieu de la partie vide. Sur Wikilivres, il y a plus de 100 ouvrages terminés entièrement libres d'usage et toujours améliorables collectivement. Deux fois plus que la cinquantaine de livres dont tu parles dans ton rêve.
:C'est normal d'abandonner un projet d'écriture. La vie est tellement imprévisible. Les ressources en temps et énergie tant limitées. Combien de personnes ont commencé un livre sur papier ou sur leur ordi sans jamais le terminer ?
:C'est pareil sur Wikilivres, sauf que c'est visible aux yeux de tous.
:Quand on commence un truc, c'est jamais dans l'intention de l'arrêter. Si on arrête, c'est qu'il y a des raisons à cela. Je n'ai donc pas envie de voir dans ces abandons des actes de malveillance, ni de critiquer leurs auteurs. J'ai d'ailleurs moi-même des projets que je n'arrive pas à concrétiser (exemple).
:Je pense au contraire que ce n'est pas un problème à partir du moment où nous avons la liberté et les outils (communication, suppression, fusion) qui nous permettent de faire le tri entre ce qui est terminé, voire même ce qui est de qualité, et le reste.
:Ce rêve est donc possible et mes démarches pour devenir wikimédien en résidence à l'université sont une démarche qui va dans ce sens. J'ai d'ailleurs apporté les preuves que l'intégration des projets Wikimédia dans le processus d'apprentissage universitaire est non seulement possible, mais hyper efficace. Malheureusement, j'ai toujours l'impression que cela n'intéresse personne. Ni à l'Unif, ni dans Wikimédia. Mais je ne désespère pas. Les changements de paradigmes sont lents en raison des habitudes. C'est une fatalité chez l'humain. Il faut faire avec.
:Mais donc. Concentrons-nous sur la manière d'organiser notre bibliothèque pour que les visiteurs puissent confortablement voir ce qui s'y trouve en séparant, livres terminés, livres de qualité (cela se fait bien avec les articles de Wikipédia), livres en cours d'édition et livres abandonnés, etc. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 13:32 (CEST)
::Je pense que nos points de vue sont irréconciliables en toute amitié. Pour moi, Wikibook est une bibliothèque de livres pédagogiques libres que chacun peut améliorer. Soit on peut mettre un étudiant sur un livre. Soit ce livre est inutile (Je parle de livre de plus de trois ans).
::.
::Wikibook n'est pas un entrepôt pour les auteurs. C'est une bibliothèque.[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 14:57 (CEST)
:::@[[Utilisateur:Xhungab|Xhungab]], c'est bien que l'on ne soit pas d'accord, car cela permet d'avancer vers une solution que tout le monde pourra approuver. C'est le principe du consensus et cela prend du temps en discussion. C'est normal et sain. En de plus, ce qui est dit ici est tout à fait pertinent pour Wikiversité qui est confronté à des problèmes similaires que Wikilivres.
:::J'exprime mes idées ci-dessous mes idées autrement pour voir si cela peut se rapprocher de ta propre vision des choses.
:::J'imagine la bibliothèque Wikilivres avec trois pièces :
:::# Une pièce principale à l'entrée qui reprend tous les livres terminés et prêts à l'emploi par une personne désireuse de les utiliser dans le cadre d'un apprentissage. Comme dans toutes les bonnes bibliothèques, ces ouvrages sont rangés par thèmes et certains d'entre eux sont mis en évidence en raison de leurs qualités, de leur pertinence par rapport à l'actualité, etc.
:::# Une pièce archives à l'arrière du bâtiment (un espace de nom que l'on pourrait choisir parmi ceux existants ou créer si nécessaire) dans laquelle on entrepose toute une série de livres abandonnés mais dont le contenu est archivé dans le but d'une éventuelle réutilisation. Cette réutilisation, comme le suggère @[[Utilisateur:DavidL|DavidL]], peut s'effectuer dans le cadre d'une fusion du contenu abandonné avec un autre contenu abandonné, ou mieux encore, si cela se présente, avec un livre terminé dans lequel les informations utiles du document abandonné sont manquantes. À noter que cela représente beaucoup de travail, tant pour la relecture que pour la mise en œuvre des fusions, qui, à mes yeux de personne expérimentée dans l'usage de MediaWiki, est un processus compliqué. De plus, il ne peut être fait que par des personnes ayant les outils d'administrateur. C'est-à-dire quatre personnes, dont deux ou trois actives dans ces récentes discussions.
:::# Dans la librairie Wikilivres, il y a ensuite un étage dans lequel chaque personne inscrite peut bénéficier d'un espace personnel, pour se présenter, stocker des informations, mais également travailler sur des projets personnels d'écriture. En tenant compte de ceci, on peut alors aussi imaginer de déplacer les ouvrages inachevés de la pièce principale à l'entrée de la librairie, vers ces différents locaux selon leurs auteurs respectifs.
:::Face à ces trois espaces, la question qu'il reste à se poser est de savoir si et dans quelles condition, on peut déplacer les ouvrages inachevés vers une salle d'archive, ou dans les locaux personnels des auteurs ? Cela en signalant que ce déplacement peut être fait très facilement et par toutes les personnes qui le souhaitent, même sans inscription à la bibliothèque. Ce qui est un grand avantage par rapport à la fusion.
:::Pendant ce temps, à l'accueil de la bibliothèque Wikibook, on invite les visiteurs à parcourir la pièce avec les ouvrages terminés, mais également la pièce archive, où ils peuvent trouver des ouvrages inachevés, au cas où ils veulent poursuivre leurs développements, ou trouver des ressources intéressantes pour leurs travaux personnels. On les informe ensuite aussi qu'avec leur inscription, ils bénéficient d'un local pour leurs activités personnelles et notamment pour y placer les ébauches d'ouvrages qu'ils désirent rédiger.
:::Que penses-tu de mon propre rêve ? Est-il si différent du tien ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 17:17 (CEST)
::::Et les autres ? @[[Utilisateur:DavidL|DavidL]], @[[Utilisateur:Fourmidable|Fourmidable]], @[[Utilisateur:JackPotte|JackPotte]], @[[Utilisateur:Grondin|Grondin]]. Que pensez-vous de nos rêves ? Et, si le mien vous parle, quelles seraient vos propositions pour orienter les déplacements de pages à l'abandon entre un espace d'archivage (qui pourrait être créé) et des espaces utilisateurs ? Le nombre d'octets ? La finalisation d'une section ou d'un chapitre ? Quelles autres possibilités ? [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 17:37 (CEST)
::::: Bonjour, a priori ça doit se régler au cas par cas sur [[Wikilivres:Demandes de suppression]]. [[Utilisateur:JackPotte|JackPotte]] ([[Discussion utilisateur:JackPotte|<span style="color:#FF6600">$</span>♠]])
::::Bonjour,
::::La structure de Wikibook est parfaite grâce à sa vitrine. Pour moi, il y a juste un problème sur les listes sur la droite : Arts, Loisir, Histoire, Langues, Technologie, Informatique, Sciences, Sciences humaines. Ces listes ne devraient donner accès qu'aux livres terminés.
::::Ces listes actuelles pourraient être caché dans un espace création qui pourrait être posé avec le lien Nouveauté en bas de la page.
::::Pour la gestion des livres, il suffirait qu'une ou deux personnes parcourent la bibliothèque et proposent quelques suppressions chaque semaine, pour en quelques mois éclaircir la situation. Je pense que l'on peut faire confiance aux administrateurs pour gérer la situation des livres, ceux qui sont à garder et les autres. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 26 août 2026 à 18:30 (CEST)
:::::@[[Utilisateur:JackPotte|JackPotte]]. Le cas par cas est ce que nous faisons depuis toujours. Cela nous amène à la situation actuelle qui semble ne pas convenir à tous. En tout cas, c'est mon cas. Ici comme sur Wikiversité. Quand, en navigant sur le site, une personne tombe neuf fois sur dix sur des pages presque vides et inachevées, comme c'est le cas sur Wikilivre, elle se dit : c'est quoi ce site ? Elle passe alors à autre chose de plus vivant, genre https://zestedesavoir.com/ ou autre. Sans changements éditoriaux, je pense que la situation évoluera.
:::::@[[Utilisateur:Xhungab|Xhungab]], on peut faire comme tu dis. Cependant, la charge de travail repose sur une double action, la proposition d'abord et le traitement ensuite. Un traitement qui finalement repose sur les épaules des administrateurs. Nous ne sommes que des bénévoles peu nombreux et pas toujours disponibles. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 26 août 2026 à 19:13 (CEST)
:Créer un livre, le ranger, en établir un plan, le catégoriser n'est pas un travail négligeable... Un travail que d'autres personnes intéressées pour écrire sur le sujet n'ont pas à refaire 🤷♂️ ...
== Petite proposition indécente à Lionel ==
Bonjour Lionel,
Je souhaiterais te proposer un petit projet.
a) Dans la page principale de wikibook tu créés une page "Espace pour les auteurs". Tu insères cette page devant le lien "Nouveautés".
.
b) Tu copies dans cette page la liste des liens qui se trouvent en haut à droite dans la nouvelle page. (Arts, Loisirs, ...) Tu ne les modifies pas dans la page principale.
.
c) Dans la nouvelle page tu peux écrire un texte du genre.
Dans cette page, vous trouverez la liste de tous les livres disponibles sur Wikibook. Vous trouverez en particulier des livres en construction et vous pourrez entrer en contact avec les auteurs. Vous trouverez aussi des livres en construction sans auteur actif que vous pourrez compléter.
Cela est la première étape, si l'aventure t'intéresse. En fait tu ajoutes juste une page, que tu pourras supprimer si les besoins se font sentir. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 27 août 2026 à 12:25 (CEST)
:La proposition est très décente @[[Utilisateur:Xhungab|Xhungab]]
:Mais avant toute choses le plus prudent est de lancer une prise dé décision pour s'assurer d'un concesnsus en faveurs des changements dont nous débatons actuellement. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 27 août 2026 à 15:40 (CEST)
::Toutes ces actions seront réversibles. On ajoute une page. Un problème, on la supprime.
::On fait rapidement les changements, on attend trois jours. En cas de problème, on supprime tout. Il suffira de s'excuser et de dire que l'on ne savait pas que l'on n'avait pas le droit.
:: Un espace pour les lecteurs et un espace pour les auteurs séparés.
::[https://fr.wikibooks.org/wiki/Utilisateur:Xhungab Nouvelle page wikibook][[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 27 août 2026 à 16:49 (CEST)
:@[[Utilisateur:Xhungab|Xhungab]] , ma grand mère disait : faire et défaire, c'est toujours travailler. Pour les grand changements, on a coutume de faire un prise de décision composée des propositions, suivies de débats et d'un vote.
:En plus, certaines décisions nécessite un changement technique et ce n'est pas toujours facilement réversible. Comme créer un nouvel espace de nom pour y transfèrer une catégorie de pages [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 28 août 2026 à 03:07 (CEST)
::Merci pour ton aide. Tu comprends maintenant pourquoi toi tu es un administrateur et moi pas.
::.
::'''Oui, aujourd'hui Wikibook peut changer. Wikibook va changer.'''
::.
::* '''Le changement c'est Maintenant :''' [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab '''Wikibook''']
::[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 28 août 2026 à 08:33 (CEST)
:::Ton enthousiasme fait plaisir @[[Utilisateur:Xhungab|Xhungab]], et je serais en faveur d'un style plus épuré de notre page d'accueil en plus d'une méthodes de classement et de séparation des ouvrages terminé, en cours et abandonnés. Je pense qu'à terme, nous pourrions faire une proposition de changement à soumettre au reste de la communauté. Par chance, nous ne sommes pas nombreux, cela simplifire les discussions et le processus de décision. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 19:26 (CEST)
::::Cette page [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab '''Wikibook'''] propose la vitrine, les livres disponibles, les mini-livres disponibles pour les lecteurs. Les lecteurs ne seront en contact qu'avec les livres terminés.
::::.
::::En bas à côté des nouveautés, il y a l' '''[https://fr.wikibooks.org/wiki/Discussion_utilisateur:Xhungab Espace pour les auteurs]''', avec tous les livres du site rangés par thème ou par niveau d'avancement si on clique sur l'image. Sinon, il y a une liste de catégories art,...
::::.
::::Cette proposition sépare donc deux espaces. Un pour les lecteurs et un pour les auteurs. Il suffirait maintenant plus qu'à nettoyer l'espace auteurs. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 20:22 (CEST)
L'usage des catégories me semble aussi une manière très pratique de trier le contenu de Wikilivre. Malheureusement, ces pages ne sont pas très sexy du coup l'usage de type de code pourrait être utile :
{ {Boîte déroulante|titre=Cliquez ici pour voir les livres terminés|alignT=left|contenu={ { #categorytree:Livres terminés|mode=pages} } } }
Avec le rendu ci-dessous :
{{Boîte déroulante|titre=Cliquez ici pour voir les livres terminés|alignT=left|contenu={{ #categorytree:Livres terminés|mode=pages}}}}
On pourrait ainsi imaginer d'intégrer directement ce code dans la page d'accueil pour afficher une boite déroulante au lieu d'envoyer le visiteur en dehors de la page d'accueil. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 21:08 (CEST)
:Tu devrais créer la page Lioneltest01 dans ta page utilisateur. Copier le code de la page principale de wikibook.
:Puis faire les changements que tu juges nécessaires. Tu peux éventuellement faire plusieurs pages. Ensuite, tu mets cette page dans le bistro pour que l'on puisse voir le résultat. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 21:22 (CEST)
::Je devrais, tu devrais, il devrait, nous devrions ... =) [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 21:28 (CEST)
:::Chaque chose en son temps.
:::1. Débattre ici des idées de changements.
:::2. Mettre les choses en place pour leur réalisation, comme la mise à jour du modèle:boite déroulante en s'inspirant de celui de Wikipédia ou d'autres sites.
:::3. Quand il n'y a plus de nouvelle idée ici, créer une page genre :[[Wikilivres:Prise de décision/Nouvelle page d'accueil et gestion des abandons]] avec une description précise des projets de changements, et une éventuelle [[Accueil/projet|page de présentation du projet d'un nouvelle page d'accueil]]
:::4. Poursuivre les discussions et propositions si nécessaire.
:::5. Quand on a fait le tour, passer au vote.
:::6. Mettre les décisions en application. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 22:03 (CEST)
::::Je propose actuellement un projet qui se trouve sur ma page utilisateur. Pourrais-tu lancer une prise de décision sur ce projet. Je ne sais pas comment on fait une telle demande. Je suis prêt à présenter mon projet et à recevoir les critiques. Merci pour ton aide. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 22:16 (CEST)
:::::''Aide-toi le ciel t'aidera''. La bureaucratie, c'est pas mon truc. Et puis ça m'irrite un peu que tu enchaines des demandes de faire des choses qui ne demande pas les outils d'administrateurs. Raisons pour lequelles, je pense finalement que l'on gagnerait à appliquer la [[w:Do-ocratie|Do-ocratie]] qui a fait ses preuves dans le monde du libre dont les projets Wikimédia sont issus.
:::::Si tu as une idée et qu'elle ne demande pas des droits de modification que tu n'as pas, fait la et on se retrouve en page de discussion de la page de tes changements en cas de problèmes et pour d'éventuelle commentaire. Sans réaction, pense à ''qui ne dit mot conscent''.
:::::Si tes idées demandes les outils d'administrateurs, tu peux alors demander à un admin de le faire. Libre à lui de le faire ou pas et si ça lui plait, si il a envie, si il prend le temps. Nous sommes bénévoles... Il y a bien des gens payés par la fondation, mais comment dire... Disons qu'ils ne sont pas des plus serviables. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 29 août 2026 à 22:57 (CEST)
::::::Excuse moi je pensais qu'il faillait être administrateur pour faire une demande de prise de décision. Je vais donc essayer tous seul. Merci pour ton aide. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 29 août 2026 à 23:06 (CEST)
:::::::Ok @[[Utilisateur:Xhungab|Xhungab]], pas de soucis. On ne peut pas tout savoir =) [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 30 août 2026 à 01:45 (CEST)
== Wikilivres:Prise de décision/Créer un espace lecteurs et un espace auteurs sur la page d'accueil ==
J'ai posé une demande de prise de décision concernant le sujet suivant.
'''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil Créer un espace lecteurs et un espace Les coulisses de Wikibook]'''
Merci pour votre participation
[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 30 août 2026 à 00:43 (CEST)
:Bonjour @[[Utilisateur:Xhungab|Xhungab]]. Content de te revoir dans les murs de Wikiversité après nos échanges sur Wikilivres. Je comprends ton sentiment de solitude, je le ressens aussi souvent. Il faut l'accepter et ne pas se décourager pour autant. Nous avons la chance de fonctionner dans une organisation bénévole, dans laquelle personne n'est tenu de répondre à des demandes. De ce fait, les choses évoluent lentement et en fonction des disponibilités. Dis-toi aussi que la période estivale est toujours un moment d'acalmie dans la participation. Moi par exemple, quand il fait bon, je préfère passer du temps sur des occupations extérieures que devant un écran.
:Cela dit, tu dois garder 3 règles implicites qui sont fréquemment appliquées dans les projets Wikimédia.
:1. ''Qui ne dit mot consent''. Si les personnes ne réagissent pas, cela ne veut pas dire qu'elles ne lisent pas. Par économie de temps et pour les personnes trop occupées ou pas assez motivées pour s'investir, ne pas réagir est un signe de consentement implicite.
:2. ''Les absents ont toujours tort.'' Lorsque l'on fait les choses dans les règles de l'art, par exemple comme tu le fais en lançant une prise de décision pour opérer un changement significatif dans Wikilivres, aucune personne absente durant un processus de décision clôturé viendra remettre en cause les choses par la suite.
:3. La doocratie. Les wikis sont des espaces de libertés au niveau de l'amélioration des contenus, mais également de l'organisation et des formes de contenus. En dehors du vandalisme qui est bien contrôlé par des personnes telles que @[[Utilisateur:Crochet.david|Crochet.david]], toute initiative est souvent perçue comme bienvenue. @[[Utilisateur:Fourmidable|Fourmidable]], par exemple, depuis qu'il est administrateur, a fait de nombreuses améliorations dans Wikiversité sans demander l'avis de la communauté. Personne ne lui a reproché son investissement. Si quelque chose qu'il a entrepris ne plaît pas à quelqu'un, une discussion commence entre lui et cette personne pour chercher une entente.
:Voilà tout.
:Je te rejoins aussi sur le fait que ce qui est décidé et mis en place sur Wikilivres peut tout à fait servir d'exemple pour Wikiversité et vice versa. Je ne suis pas le seul à être administrateur dans les deux projets et cela aide beaucoup à la création d'une synergie entre les deux projets qui sont finalement très complémentaires. Cela n'est pas étonnant d'ailleurs lorsque l'on sait que Wikiversité est né de Wikilivres.
:Une belle fin de journée à toi et à tous ceux qui liront ce message. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 6 septembre 2026 à 13:30 (CEST)
::Merci. Pour tes explications. [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 6 septembre 2026 à 13:50 (CEST)
:::Avec plaisir. Pense aussi qu'il n'y a pas de combat dans les projets Wiki, mais un lent processus évolutif, sans fin programmée, et qui est pour moi l'un des meilleurs que je connaisse. [[User:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[User talk:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 6 septembre 2026 à 13:57 (CEST)
== Les RAW sont arrivés, la rentrée peut commencer ! ==
<div style="background-color:#177860; border-radius:.2em; color:#FFF;padding: 1em;">
<div style="font-family: Century Gothic; font-size:3em; line-height:120%;text-align:center;color: #fff;">RAW</div>
<div style="margin-bottom:1.5em; margin-top:1em; text-align:center; color: #fff;">Le numéro de septembre 2026 ({{numéro|296}}) est sorti.</div>
<div class="center">[[:w:Wikipédia:RAW/2026-09-05|<span class="cdx-button cdx-button--fake-button cdx-button--fake-button--enabled" style="white-space:break-spaces; text-align:center; gap:0;"role="button" aria-disabled="false">Lire l'infolettre</span>]]</div>
</div>
'''C'est la rentrée'''
Après un numéro très dense en août en raison de la Wikimania, celui-ci est un peu plus léger car l'équipe s'est octroyée quelques vacances. Cela n'empêche pas le mouvement de continuer à vivre, avec une rentrée déjà animée ! Du côté des projets francophones, le Wiktionnaire vient de franchir le cap symbolique des sept millions d’entrées, tandis que Wikipédia continue de voir fleurir sondages et discussions sur son fonctionnement.
Une rentrée, donc, placée sous le signe de la construction collective : de nouveaux outils pour contribuer, de nouveaux espaces pour échanger, de nouvelles formes d’organisation… Ce numéro des RAW vous propose de faire le tour de cette actualité, avant de reprendre tranquillement le chemin des projets Wikimédia.
L'équipe de la rédaction a également commencé à réfléchir à un nouveau nom pour remplacer l'énigmatique « RAW ». N'hésitez pas à venir donner votre avis dans la [[:w:Discussion Wikipédia:RAW/Rédaction#Changement de nom|section dédiée]] !
----
Au menu de ce numéro :
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">En bref</span>''' — Des actualités francophones et du mouvement Wikimédia, des nouveaux outils et des articles de recherche
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">Le wik'hit parade</span>''' — Une analyse des articles les plus vus le mois dernier
[[Fichier:Toicon-icon-sharp-corners-draw.svg|20px|classe=skin-invert-image]] Les RAW, c'est aussi vous : n'hésitez pas à suggérer des sujets ou participer à la rédaction du [[:w:Wikipédia:RAW/Rédaction|prochain numéro]] !
[[Utilisateur:Antimuonium|Antimuonium]] ([[Discussion utilisateur:Antimuonium|discussion]]) 5 septembre 2026 à 00:17 (CEST)
== Wikilivres:Prise de décision/Modifier le contenue de l'espace Nouveautés ==
J'ai posé une demande de prise de décision concernant le sujet suivant.
[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Modifier_le_contenue_de_l%27espace_Nouveaut%C3%A9s Wikilivres:Prise de décision/Modifier le contenue de l'espace Nouveautés]
Merci pour votre participation [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 9 septembre 2026 à 14:38 (CEST)
== '''Décision''':Prise de décision/Créer un espace lecteurs et un espace auteurs sur la page d'accueil ==
J'ai posé une demande de prise de décision concernant le sujet suivant.
'''[https://fr.wikibooks.org/wiki/Wikilivres:Prise_de_d%C3%A9cision/Cr%C3%A9er_un_espace_lecteurs_et_un_espace_auteurs_sur_la_page_d%27accueil Créer un espace lecteurs et un espace Les coulisses de Wikibook]'''
'''Décision pour : [https://fr.wikibooks.org/wiki/Utilisateur:Xhungab '''Wikibook''']'''
Résultats : 3 + Pour, un Neutre Neutre et un - Contre soit 60 % de + Pour par rapport au total des votes exprimés : la proposition est adoptée. Quelques propositions de reformulations et de nuances ont été faites qui pourront être prise en compte. Cdlt, VIGNERON * discut. 13 septembre 2026 à 18:27 (CEST)
Merci pour votre participation. Ma première proposition était un peu malhabile, mais grâce à vos interventions je pense que l'on a créé quelque chose de correct. '''Merci à tous''' [[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 13 septembre 2026 à 20:22 (CEST)
== Je ne suis donc pas content tous seul. (2) ==
Discutons dans la page d'accueil.
'''[https://fr.wikibooks.org/wiki/Discussion:Accueil#Je_ne_suis_donc_pas_content_tous_seul._%282%29 Je ne suis pas content tous seul]''' Version 2 dans la page d'accueil.
[[Utilisateur:Xhungab|Xhungab]] ([[Discussion utilisateur:Xhungab|discussion]]) 17 septembre 2026 à 14:28 (CEST)
:Je replace ici un message précédent que j'avais maladroitement supprimé :
:: Bonjour @[[Utilisateur:Xhungab|Xhungab]].
:: Nous tentons de fonctionner au consensus dans les projets Wikimédia. On essaie donc de faire les choses avec un consentement général. La prise de décision a été rapide, les discussions inachevées, et certaines personnes n'étaient pas satisfaite de tes propositions. Comme la cloture de la décision a été faite, je me suis mis au travail.
:: Il est ensuite important de signaler que dans [[Discussion utilisateur:Lionel Scheepmans#Décision:Prise de décision/Créer un espace lecteurs et un espace auteurs sur la page d'accueil|une précédente discussion]], je suis resté ouvert à tout changement de la page d'accueil à partir de ce que j'ai considéré être le plus rationnel au regard de tout ce qui existait déjà. C'est-à-dire [https://fr.wikibooks.org/wiki/Sp%C3%A9cial:Index?prefix=&namespace=4 de nombreuses pages] créées et améliorées par le passé que nous ne pouvons pas ignorer au risque de créer des doublons.
:: Si tu es insatisfait du résultat, je te propose de nous retrouver, comme cela a déjà été proposé aussi dans notre précédente discussion, sur la [[Discussion:Accueil|page de discussion de la page d'accueil]] qui est le meilleurs endroit pour discuter de son contenu tout en étant sûr que ces discussion ne se perdent pas dans un archivage, comme c'est le cas pour le bistro. On peut donc s'y retrouver, avec toutes les autres personnes qui ont des propositions d'améliorations.
:: Du reste, nous sommes tous bénévoles ici. Y compris les administrateurs. Pour ma part, être bénévole, c'est faire les choses par sa propre volonté et avec bienveillance. C'est être volontaire bienveillant. Pour ma part, je reste toujours bienveillant. Ma volonté, quant à elle, peut changer en fonction de mes convictions et motivations.
:: Une belle fin de journée à tous ! [[Utilisateur:Lionel Scheepmans|Lionel Scheepmans]] <sup><big>✉</big> [[Discussion utilisateur:Lionel Scheepmans|Contact]]</sup> <sub>Désolé pour ma [[w:dysorthographie|dysorthographie]], [[w:dyslexie|dyslexie]] et [[wikt:distraction|"dys"traction]].</sub> 17 septembre 2026 à 13:20 (CEST)
== Des RAW tout frais, à lire sans délai ! ==
<div style="background-color:#177860; border-radius:.2em; color:#FFF;padding: 1em;">
<div style="font-family: Century Gothic; font-size:3em; line-height:120%;text-align:center;color: #fff;">RAW</div>
<div style="margin-bottom:1.5em; margin-top:1em; text-align:center; color: #fff;">Le numéro d'octobre 2026 ({{numéro|297}}) est sorti.</div>
<div class="center">[[:w:Wikipédia:RAW/2026-10-05|<span class="cdx-button cdx-button--fake-button cdx-button--fake-button--enabled" style="white-space:break-spaces; text-align:center; gap:0;"role="button" aria-disabled="false">Lire l'infolettre</span>]]</div>
</div>
'''Contribuer, chacun à sa manière'''
Dans une grande partie de la francophonie, l'automne pointe le bout de son nez, et avec lui un nouveau numéro des RAW !
Ce numéro fait la part belle à la contribution sous plusieurs formes : des campagnes de contribution et des ateliers pour améliorer des milliers d'articles sur divers sujets, des discussions communautaires, des élections, des événements, des nouveautés techniques, et même une nouvelle publication francophone ! La contribution peut aussi prendre des formes plus inattendues : pourquoi pas à travers des jeux ? Et pour découvrir plus concrètement ce que signifie contribuer à Wikipédia, nous nous intéresserons au parcours d'un utilisateur fraîchement élu administrateur : Nanoyo88.
Par ailleurs, la réflexion sur le changement de titre des RAW avance : les deux propositions favorites sont ''La Wikigazette'' et ''La Gazette de Wikimédia''. N'hésitez pas à [[w:Discussion Wikipédia:RAW/Rédaction#Deuxième tour|donner votre avis]].
Bonne lecture, et surtout, bonnes contributions !
----
Au menu de ce numéro :
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">En bref</span>''' — Des actualités francophones et du mouvement Wikimédia, des nouveaux outils et des articles de recherche
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">L'Interview</span>''' — Nanoyo88 : de débutant à administrateur, pour une Wikipédia plus accessible
'''<span style="margin:0;background:var(--color-success, #177860);padding:3px;color:white">Le wik'hit parade</span>''' — Une analyse des articles les plus vus le mois dernier
[[Fichier:Toicon-icon-sharp-corners-draw.svg|20px|classe=skin-invert-image]] Les RAW, c'est aussi vous : n'hésitez pas à suggérer des sujets ou participer à la rédaction du [[w:Wikipédia:RAW/Rédaction|prochain numéro]] !
[[Utilisateur:Antimuonium|Antimuonium]] ([[Discussion utilisateur:Antimuonium|discussion]]) 5 octobre 2026 à 00:06 (CEST)
1zeo1cve2pb7t1ryegkjfm5ceazz0bb
Mathc initiation/007n
0
84562
774486
2026-10-09T14:42:44Z
Xhungab
23827
news
774486
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les series de Fourier avec f : -> x^2}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/007h|f2.h ]] ...... '''f(x) = x^2 avec x = [0..PI]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/007i|c2a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/007j|c2an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/007k|c2bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/007l|c2fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/a9a|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/007m|c2fsN.c]]
{{AutoCat}}
73a2on7maya7klbn8cbl64bpc4ueg3y
774488
774486
2026-10-09T14:46:05Z
Xhungab
23827
774488
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les series de Fourier avec f : -> x^2}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/007h|f2.h ]] ...... '''f(x) = x^2 avec x = [0..PI]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/007i|c2a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/007j|c2an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/007k|c2bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/007l|c2fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/007m|c2fsN.c]]
{{AutoCat}}
bx43mpqpymuxle2bwx9aequ3onyvqy3
774494
774488
2026-10-09T14:50:22Z
Xhungab
23827
774494
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les series de Fourier avec f : -> x^2}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/007h|f2.h ]] ...... '''f(t) = t^2 avec t = [-PI..PI]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/007i|c2a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/007j|c2an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/007k|c2bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/007l|c2fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/007m|c2fsN.c]]
{{AutoCat}}
qcl8qlca3au0lzvwsji6vw2zq8sftwb
774511
774494
2026-10-09T15:23:21Z
Xhungab
23827
774511
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les series de Fourier avec f : -> x^2}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/007h|f2.h ]] ...... '''f(t) = t^2 avec t = [-PI..PI]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/007i|c2a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/007j|c2an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/007k|c2bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
{| class="wikitable"
|+ Séries de Fourier
|-
! plot[-pi:pi][-1.:10] !! plot[-pi:4*pi][-1.:10]
|-
| [[File:Fourier serie for my wikibook 2.png|thumb|]] ||
[[File:Fourier serie for my wikibook 3.png|thumb|]]
|}
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les 9 premiers terme'''s de la série.
* [[Mathc initiation/007l|c2fs.c]]
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les N premiers terme'''s de la série.
* [[Mathc initiation/007m|c2fsN.c]]
{{AutoCat}}
edz518f2wuy6p87tx66nm1c34i3y4ro
774576
774511
2026-10-10T08:36:42Z
Xhungab
23827
774576
wikitext
text/x-wiki
__NOTOC__
[[Catégorie:Mathc initiation (livre)]]
:
[[Mathc initiation/a594| Sommaire]]
:
:
{{Partie{{{type|}}}|Se familiariser avec les series de Fourier avec f : -> x^2}}
:
.
:
Copier la bibliothèque dans votre répertoire de travail :
* [[Mathc initiation/a599|x_hfile.h ............. Déclaration des fichiers h]]
* [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]]
* [[Mathc initiation/a600|x_fcoef.h ........... Coefficients de Fourier]]
:
.
:
La fonction f :
* [[Mathc initiation/007h|f2.h ]] ...... '''f(t) = t^2 avec t = [-PI..PI]'''
:
.
:
Les séries de Fourier :
* [[Mathc initiation/007i|c2a0.c]] ..... '''Coefficient a0''' ... ... <math> \mathbf{a_0(f)=\frac1T\int_Tf(t)\,\mathrm{d}t}</math>
* [[Mathc initiation/007j|c2an.c]] ..... '''Coefficient an''' ... ... <math>\mathbf{a_n(f)=\frac2T\int_Tf(t)\cos\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
* [[Mathc initiation/007k|c2bn.c]] ..... '''Coefficient bn''' ... ... <math>\mathbf{b_n(f)=\frac2T\int_Tf(t)\sin\left(nt\frac{2\pi}T\right)\,\mathrm dt}</math>
.
{| class="wikitable"
|+ Séries de Fourier
|-
! plot[-pi:pi][-1.:10] !! plot[-pi:4*pi][-1.:10]
|-
| [[File:Fourier serie for my wikibook 2.png|thumb|]] ||
[[File:Fourier serie for my wikibook 3.png|thumb|]]
|}
Avec ce fichier C nous allons pouvoir dessiner avec '''[[Mathc initiation/007o|gnuplot]]''' '''les 9 premiers termes''' de la série.
* [[Mathc initiation/007l|c2fs.c]]
Avec ce fichier C nous allons pouvoir dessiner '''les N premiers termes''' de la série.
* [[Mathc initiation/007m|c2fsN.c]]
{{AutoCat}}
gm7whqcioolqcfush67dnyp57q4lg87
Mathc initiation/007o
0
84563
774489
2026-10-09T14:46:41Z
Xhungab
23827
news
774489
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a593| Sommaire]]
'''Fichier de commande gnuplot (1) : '''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-pi:pi] [-1.:10]\
(x*x) lt 9 lw 5,\
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x) lt 4 lw 3
reset
</syntaxhighlight>
'''Fichier de commande gnuplot (2) :'''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-pi:4*pi] [-1.:10]\
(x*x) lt 9 lw 5,\
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x) lt 4 lw 3
reset
</syntaxhighlight>
{{AutoCat}}
pv2bmqm108jxty6uncf1226bl36q2ml
774491
774489
2026-10-09T14:47:22Z
Xhungab
23827
774491
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
'''Fichier de commande gnuplot (1) : '''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-pi:pi] [-1.:10]\
(x*x) lt 9 lw 5,\
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x) lt 4 lw 3
reset
</syntaxhighlight>
'''Fichier de commande gnuplot (2) :'''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-pi:4*pi] [-1.:10]\
(x*x) lt 9 lw 5,\
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x) lt 4 lw 3
reset
</syntaxhighlight>
{{AutoCat}}
p5qdvox8lu57segsmi7lmwypjpmv0a0
774575
774491
2026-10-10T08:35:52Z
Xhungab
23827
774575
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
'''Fichier de commande gnuplot (1) : '''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-pi:pi] [-1.:10]\
(x*x) lt 9 lw 5,\
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x)\
lt 4 lw 3
reset
</syntaxhighlight>
'''Fichier de commande gnuplot (2) :'''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-pi:4*pi] [-1.:10]\
(x*x) lt 9 lw 5,\
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x)\
lt 4 lw 3
reset
</syntaxhighlight>
{{AutoCat}}
rwowfbi1dgw4uc2vl4qyntu2yajjs2w
Mathc initiation/007h
0
84564
774493
2026-10-09T14:49:31Z
Xhungab
23827
news
774493
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer ce fichier dans votre répertoire de travail.
{{Fichier|f2.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as f2.h */
/* ---------------------------------- */
double f(
double t)
{
return(t*t);
}
char feq[] = "(t^2))";
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
{{AutoCat}}
4fjpv0e1k0m1ht4edztfs3ne2sr7cbi
Mathc initiation/007i
0
84565
774497
2026-10-09T14:55:21Z
Xhungab
23827
news
774497
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2a0.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2a0.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double a = -PI;
double b = +PI;
double a0 = 1./T * fourier_a0(f, a, b);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" (a + T\n"
" a_0 = 1/T int( (%s) dx = %.6f\n"
" (a \n\n\n\n", feq, a0);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
(a + T
a_0 = 1/T int( ((t^2))) dx = 3.289868
(a
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
rhnjkbytjzd7ci072bwfh8et9fy9tva
Mathc initiation/007j
0
84566
774498
2026-10-09T14:57:02Z
Xhungab
23827
news
774498
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2an.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2an.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double omega = (2.*PI/T);
double a = -PI;
double b = +PI;
double n = 1.; /* You can change the value of n */
double an = 2./T * fourier_an(f, a,b, omega, n);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" (a + T\n"
" a_%.0f = 2/T int( (%s) cos(%.0f omega t) dt = %.6f\n"
" (a \n\n\n\n",n, feq, n, an);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
(a + T
a_1 = 2/T int( ((t^2))) cos(1 omega t) dt = -4.000000
(a
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
94iu23tnguq0c7l9o8kpfxc4b5v3o9l
Mathc initiation/007k
0
84567
774500
2026-10-09T15:01:53Z
Xhungab
23827
news
774500
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2bn.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2bn.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double omega = (2.*PI/T);
double a = -PI;
double b = +PI;
double n = 1.; /* You can change the value of n */
double bn = 2./T * fourier_bn(f, a,b, omega, n);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" (a + T\n"
" b_%.0f = 2/T int( (%s) sin(%.0f omega t) dt = %.6f\n"
" (a \n\n\n\n",n, feq, n, bn);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
(a + T
b_1 = 2/T int( ((t^2))) sin(1 omega t) dt = 0.000000
(a
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
dikboi921i63ekeclcd2agmz6iao6f6
Mathc initiation/007l
0
84568
774502
2026-10-09T15:03:57Z
Xhungab
23827
news
774502
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2fs.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2fs.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double omega = (2.*PI/T);
double a = -PI;
double b = +PI;
double a0 = 1./T * fourier_a0(f, a,b);
double a1 = 2./T * fourier_an(f, a,b, omega, 1);
double a2 = 2./T * fourier_an(f, a,b, omega, 2);
double a3 = 2./T * fourier_an(f, a,b, omega, 3);
double a4 = 2./T * fourier_an(f, a,b, omega, 4);
double a5 = 2./T * fourier_an(f, a,b, omega, 5);
double a6 = 2./T * fourier_an(f, a,b, omega, 6);
double a7 = 2./T * fourier_an(f, a,b, omega, 7);
double a8 = 2./T * fourier_an(f, a,b, omega, 8);
double b1 = 2./T * fourier_bn(f, a,b, omega, 1);
double b2 = 2./T * fourier_bn(f, a,b, omega, 2);
double b3 = 2./T * fourier_bn(f, a,b, omega, 3);
double b4 = 2./T * fourier_bn(f, a,b, omega, 4);
double b5 = 2./T * fourier_bn(f, a,b, omega, 5);
double b6 = 2./T * fourier_bn(f, a,b, omega, 6);
double b7 = 2./T * fourier_bn(f, a,b, omega, 7);
double b8 = 2./T * fourier_bn(f, a,b, omega, 8);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" a0 = %.6f\n\n\n",a0);
printf(" %+.9f\\\n"
" %+.9f*cos(%.9f* 1*x) %+.9f*sin(%.9f* 1*x)\\\n"
" %+.9f*cos(%.9f* 2*x) %+.9f*sin(%.9f* 2*x)\\\n"
" %+.9f*cos(%.9f* 3*x) %+.9f*sin(%.9f* 3*x)\\\n"
" %+.9f*cos(%.9f* 4*x) %+.9f*sin(%.9f* 4*x)\\\n"
" %+.9f*cos(%.9f* 5*x) %+.9f*sin(%.9f* 5*x)\\\n"
" %+.9f*cos(%.9f* 6*x) %+.9f*sin(%.9f* 6*x)\\\n"
" %+.9f*cos(%.9f* 7*x) %+.9f*sin(%.9f* 7*x)\\\n"
" %+.9f*cos(%.9f* 8*x) %+.9f*sin(%.9f* 8*x)\n\n",
a0,
a1,omega, b1,omega,
a2,omega, b2,omega,
a3,omega, b3,omega,
a4,omega, b4,omega,
a5,omega, b5,omega,
a6,omega, b6,omega,
a7,omega, b7,omega,
a8,omega, b8,omega);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 3.289868
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x)
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
q2g4k832z2ctzcwiivcz186tjabbeu9
774503
774502
2026-10-09T15:05:33Z
Xhungab
23827
774503
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2fs.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2fs.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double omega = (2.*PI/T);
double a = -PI;
double b = +PI;
double a0 = 1./T * fourier_a0(f, a,b);
double a1 = 2./T * fourier_an(f, a,b, omega, 1);
double a2 = 2./T * fourier_an(f, a,b, omega, 2);
double a3 = 2./T * fourier_an(f, a,b, omega, 3);
double a4 = 2./T * fourier_an(f, a,b, omega, 4);
double a5 = 2./T * fourier_an(f, a,b, omega, 5);
double a6 = 2./T * fourier_an(f, a,b, omega, 6);
double a7 = 2./T * fourier_an(f, a,b, omega, 7);
double a8 = 2./T * fourier_an(f, a,b, omega, 8);
double b1 = 2./T * fourier_bn(f, a,b, omega, 1);
double b2 = 2./T * fourier_bn(f, a,b, omega, 2);
double b3 = 2./T * fourier_bn(f, a,b, omega, 3);
double b4 = 2./T * fourier_bn(f, a,b, omega, 4);
double b5 = 2./T * fourier_bn(f, a,b, omega, 5);
double b6 = 2./T * fourier_bn(f, a,b, omega, 6);
double b7 = 2./T * fourier_bn(f, a,b, omega, 7);
double b8 = 2./T * fourier_bn(f, a,b, omega, 8);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" a0 = %.6f\n\n\n",a0);
printf(" %+.9f\\\n"
" %+.9f*cos(%.9f* 1*x) %+.9f*sin(%.9f* 1*x)\\\n"
" %+.9f*cos(%.9f* 2*x) %+.9f*sin(%.9f* 2*x)\\\n"
" %+.9f*cos(%.9f* 3*x) %+.9f*sin(%.9f* 3*x)\\\n"
" %+.9f*cos(%.9f* 4*x) %+.9f*sin(%.9f* 4*x)\\\n"
" %+.9f*cos(%.9f* 5*x) %+.9f*sin(%.9f* 5*x)\\\n"
" %+.9f*cos(%.9f* 6*x) %+.9f*sin(%.9f* 6*x)\\\n"
" %+.9f*cos(%.9f* 7*x) %+.9f*sin(%.9f* 7*x)\\\n"
" %+.9f*cos(%.9f* 8*x) %+.9f*sin(%.9f* 8*x)\n\n",
a0,
a1,omega, b1,omega,
a2,omega, b2,omega,
a3,omega, b3,omega,
a4,omega, b4,omega,
a5,omega, b5,omega,
a6,omega, b6,omega,
a7,omega, b7,omega,
a8,omega, b8,omega);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 3.289868
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x)
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
6awu8p93jv15iq77t8v25gi1zvpnp98
774506
774503
2026-10-09T15:17:15Z
Xhungab
23827
774506
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2fs.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2fs.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double omega = (2.*PI/T);
double a = -PI;
double b = +PI;
double a0 = 1./T * fourier_a0(f, a,b);
double a1 = 2./T * fourier_an(f, a,b, omega, 1);
double a2 = 2./T * fourier_an(f, a,b, omega, 2);
double a3 = 2./T * fourier_an(f, a,b, omega, 3);
double a4 = 2./T * fourier_an(f, a,b, omega, 4);
double a5 = 2./T * fourier_an(f, a,b, omega, 5);
double a6 = 2./T * fourier_an(f, a,b, omega, 6);
double a7 = 2./T * fourier_an(f, a,b, omega, 7);
double a8 = 2./T * fourier_an(f, a,b, omega, 8);
double b1 = 2./T * fourier_bn(f, a,b, omega, 1);
double b2 = 2./T * fourier_bn(f, a,b, omega, 2);
double b3 = 2./T * fourier_bn(f, a,b, omega, 3);
double b4 = 2./T * fourier_bn(f, a,b, omega, 4);
double b5 = 2./T * fourier_bn(f, a,b, omega, 5);
double b6 = 2./T * fourier_bn(f, a,b, omega, 6);
double b7 = 2./T * fourier_bn(f, a,b, omega, 7);
double b8 = 2./T * fourier_bn(f, a,b, omega, 8);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" a0 = %.6f\n\n\n",a0);
printf(" %+.9f\\\n"
" %+.9f*cos(%.9f* 1*x) %+.9f*sin(%.9f* 1*x)\\\n"
" %+.9f*cos(%.9f* 2*x) %+.9f*sin(%.9f* 2*x)\\\n"
" %+.9f*cos(%.9f* 3*x) %+.9f*sin(%.9f* 3*x)\\\n"
" %+.9f*cos(%.9f* 4*x) %+.9f*sin(%.9f* 4*x)\\\n"
" %+.9f*cos(%.9f* 5*x) %+.9f*sin(%.9f* 5*x)\\\n"
" %+.9f*cos(%.9f* 6*x) %+.9f*sin(%.9f* 6*x)\\\n"
" %+.9f*cos(%.9f* 7*x) %+.9f*sin(%.9f* 7*x)\\\n"
" %+.9f*cos(%.9f* 8*x) %+.9f*sin(%.9f* 8*x)\n\n",
a0,
a1,omega, b1,omega,
a2,omega, b2,omega,
a3,omega, b3,omega,
a4,omega, b4,omega,
a5,omega, b5,omega,
a6,omega, b6,omega,
a7,omega, b7,omega,
a8,omega, b8,omega);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
[[File:Fourier serie for my wikibook 2.png|thumb|Fourier serie f(x) = x^2 with x[-Pi..Pi]]]
[[File:Fourier serie for my wikibook 3.png|thumb|Fourier serie f(x) = x^2 with x[-Pi..Pi]]]
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 3.289868
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x)
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
lbddt3d549jks6w1kp18e0k5xnuknt9
774507
774506
2026-10-09T15:18:59Z
Xhungab
23827
774507
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2fs.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2fs.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double omega = (2.*PI/T);
double a = -PI;
double b = +PI;
double a0 = 1./T * fourier_a0(f, a,b);
double a1 = 2./T * fourier_an(f, a,b, omega, 1);
double a2 = 2./T * fourier_an(f, a,b, omega, 2);
double a3 = 2./T * fourier_an(f, a,b, omega, 3);
double a4 = 2./T * fourier_an(f, a,b, omega, 4);
double a5 = 2./T * fourier_an(f, a,b, omega, 5);
double a6 = 2./T * fourier_an(f, a,b, omega, 6);
double a7 = 2./T * fourier_an(f, a,b, omega, 7);
double a8 = 2./T * fourier_an(f, a,b, omega, 8);
double b1 = 2./T * fourier_bn(f, a,b, omega, 1);
double b2 = 2./T * fourier_bn(f, a,b, omega, 2);
double b3 = 2./T * fourier_bn(f, a,b, omega, 3);
double b4 = 2./T * fourier_bn(f, a,b, omega, 4);
double b5 = 2./T * fourier_bn(f, a,b, omega, 5);
double b6 = 2./T * fourier_bn(f, a,b, omega, 6);
double b7 = 2./T * fourier_bn(f, a,b, omega, 7);
double b8 = 2./T * fourier_bn(f, a,b, omega, 8);
clrscrn();
printf(" _+oo_ \n"
" \\ \n"
" S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)\n"
" /_____ \n"
" n = 1 \n\n\n\n");
printf(" a0 = %.6f\n\n\n",a0);
printf(" %+.9f\\\n"
" %+.9f*cos(%.9f* 1*x) %+.9f*sin(%.9f* 1*x)\\\n"
" %+.9f*cos(%.9f* 2*x) %+.9f*sin(%.9f* 2*x)\\\n"
" %+.9f*cos(%.9f* 3*x) %+.9f*sin(%.9f* 3*x)\\\n"
" %+.9f*cos(%.9f* 4*x) %+.9f*sin(%.9f* 4*x)\\\n"
" %+.9f*cos(%.9f* 5*x) %+.9f*sin(%.9f* 5*x)\\\n"
" %+.9f*cos(%.9f* 6*x) %+.9f*sin(%.9f* 6*x)\\\n"
" %+.9f*cos(%.9f* 7*x) %+.9f*sin(%.9f* 7*x)\\\n"
" %+.9f*cos(%.9f* 8*x) %+.9f*sin(%.9f* 8*x)\n\n",
a0,
a1,omega, b1,omega,
a2,omega, b2,omega,
a3,omega, b3,omega,
a4,omega, b4,omega,
a5,omega, b5,omega,
a6,omega, b6,omega,
a7,omega, b7,omega,
a8,omega, b8,omega);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 3.289868
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x)
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
6awu8p93jv15iq77t8v25gi1zvpnp98
Mathc initiation/007m
0
84569
774504
2026-10-09T15:08:21Z
Xhungab
23827
news
774504
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/007n| Sommaire]]
Installer et compiler ces fichiers dans votre répertoire de travail.
{{Fichier|c2fsN.c|largeur=70%|info=|icon=Crystal128-source-c.svg}}
<syntaxhighlight lang="c">
/* ---------------------------------- */
/* save as c2fsN.c */
/* ---------------------------------- */
#include "x_hfile.h"
/* ---------------------------------- */
#define N 50
/* ---------------------------------- */
#include "f2.h"
/* ---------------------------------- */
int main(void)
{
double T = 2*PI;
double omega = (2.*PI/T);
double a = -PI;
double b = +PI;
double A[N+1];
double B[N+1];
int n = 0;
A[0] = 1./T * fourier_a0(f, a,b);
for( n=1; n<=N; n++)
{
A[n] = 2./T * fourier_an(f, a,b, omega, n);
B[n] = 2./T * fourier_bn(f, a,b, omega, n);
}
clrscrn();
printf(" %+.9f\\\n", A[0]);
for( n=1; n<N; n++)
printf(" %+.9f*cos(%.9f* %d*x) %+.9f*sin(%.9f* %d*x)\\\n",
A[n], omega, n,
B[n], omega, n);
printf(" %+.9f*cos(%.9f* %d*x) %+.9f*sin(%.9f* %d*x)\n",
A[n], omega, n,
B[n], omega, n);
stop();
return 0;
}
/* ---------------------------------- */
/* ---------------------------------- */
</syntaxhighlight>
'''Exemple de sortie écran :'''
<syntaxhighlight lang="C">
_+oo_
\
S(t) = a0 + | an cos(omega* n*t) + bn sin(omega *n*t)
/_____
n = 1
a0 = 3.289868
+3.289868134\
-3.999999935*cos(1.000000000* 1*x) +0.000000000*sin(1.000000000* 1*x)\
+0.999999740*cos(1.000000000* 2*x) +0.000000000*sin(1.000000000* 2*x)\
-0.444443859*cos(1.000000000* 3*x) -0.000000000*sin(1.000000000* 3*x)\
+0.249998958*cos(1.000000000* 4*x) +0.000000000*sin(1.000000000* 4*x)\
-0.159998369*cos(1.000000000* 5*x) +0.000000000*sin(1.000000000* 5*x)\
+0.111108757*cos(1.000000000* 6*x) -0.000000000*sin(1.000000000* 6*x)\
-0.081629440*cos(1.000000000* 7*x) -0.000000000*sin(1.000000000* 7*x)\
+0.062495791*cos(1.000000000* 8*x) -0.000000000*sin(1.000000000* 8*x)\
-0.049377372*cos(1.000000000* 9*x) +0.000000000*sin(1.000000000* 9*x)\
...
Press return to continue.
</syntaxhighlight>
{{AutoCat}}
dahi47dayptlz2grvmnt3jn0izjqv0b
Mathc initiation/007p
0
84570
774517
2026-10-09T15:48:34Z
Xhungab
23827
news
774517
wikitext
text/x-wiki
[[Catégorie:Mathc initiation (livre)]]
[[Mathc initiation/a602| Sommaire]]
'''Fichier de commande gnuplot (1) : '''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-1:1] [-.5:2.]\
(x*(x+1)),\
+0.333333333\
-0.405284728*cos(3.141592654* 1*x) +0.636619776*sin(3.141592654* 1*x)\
+0.101321157*cos(3.141592654* 2*x) -0.318309914*sin(3.141592654* 2*x)\
-0.045031578*cos(3.141592654* 3*x) +0.212206684*sin(3.141592654* 3*x)\
+0.025330190*cos(3.141592654* 4*x) -0.159155164*sin(3.141592654* 4*x)\
-0.016211224*cos(3.141592654* 5*x) +0.127324386*sin(3.141592654* 5*x)\
+0.011257671*cos(3.141592654* 6*x) -0.106104043*sin(3.141592654* 6*x)\
-0.008270792*cos(3.141592654* 7*x) +0.090946870*sin(3.141592654* 7*x)\
+0.006332148*cos(3.141592654* 8*x) -0.079579249*sin(3.141592654* 8*x)
reset
</syntaxhighlight>
'''Fichier de commande gnuplot (2) avec n = 50: '''
<syntaxhighlight lang="gnuplot">
# ---------------------
# Copy and past this file into the screen of gnuplot
#
#
plot [-1:4] [-.5:2.]\
(x*(x+1)),\
+0.333333333\
-0.405284728*cos(3.141592654* 1*x) +0.636619776*sin(3.141592654* 1*x)\
+0.101321157*cos(3.141592654* 2*x) -0.318309914*sin(3.141592654* 2*x)\
-0.045031578*cos(3.141592654* 3*x) +0.212206684*sin(3.141592654* 3*x)\
+0.025330190*cos(3.141592654* 4*x) -0.159155164*sin(3.141592654* 4*x)\
-0.016211224*cos(3.141592654* 5*x) +0.127324386*sin(3.141592654* 5*x)\
+0.011257671*cos(3.141592654* 6*x) -0.106104043*sin(3.141592654* 6*x)\
-0.008270792*cos(3.141592654* 7*x) +0.090946870*sin(3.141592654* 7*x)\
+0.006332148*cos(3.141592654* 8*x) -0.079579249*sin(3.141592654* 8*x)
...
reset
</syntaxhighlight>
{{AutoCat}}
hqvqd5mskfbfwo2mzje62lzgw3tekrv